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.common.utils.StringUtils.*;
020import static org.apache.juneau.common.utils.Utils.*;
021import static org.apache.juneau.http.remote.RemoteUtils.*;
022import static org.apache.juneau.httppart.HttpPartType.*;
023
024import java.lang.reflect.*;
025import java.util.*;
026import java.util.function.*;
027
028import org.apache.juneau.*;
029import org.apache.juneau.common.utils.*;
030import org.apache.juneau.http.annotation.*;
031import org.apache.juneau.http.remote.*;
032import org.apache.juneau.httppart.*;
033import org.apache.juneau.httppart.bean.*;
034import org.apache.juneau.internal.*;
035import org.apache.juneau.reflect.*;
036
037/**
038 * Contains the meta-data about a Java method on a REST proxy class.
039 *
040 * <p>
041 * Captures the information in {@link RemoteOp @RemoteOp} annotations for caching and reuse.
042 *
043 * <h5 class='section'>See Also:</h5><ul>
044 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestProxyBasics">REST Proxy Basics</a>
045 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestClientBasics">juneau-rest-client Basics</a>
046 * </ul>
047 */
048public class RemoteOperationMeta {
049
050   private final String httpMethod;
051   private final String fullPath;
052   private final RemoteOperationArg[] pathArgs, queryArgs, headerArgs, formDataArgs;
053   private final RemoteOperationBeanArg[] requestArgs;
054   private final RemoteOperationArg contentArg;
055   private final RemoteOperationReturn methodReturn;
056   private final Class<?>[] exceptions;
057
058   /**
059    * Constructor.
060    *
061    * @param parentPath The absolute URI of the REST interface backing the interface proxy.
062    * @param m The Java method.
063    * @param defaultMethod The default HTTP method if not specified through annotation.
064    */
065   public RemoteOperationMeta(final String parentPath, Method m, String defaultMethod) {
066      Builder b = new Builder(parentPath, m, defaultMethod);
067      this.httpMethod = b.httpMethod;
068      this.fullPath = b.fullPath;
069      this.pathArgs = b.pathArgs.toArray(new RemoteOperationArg[b.pathArgs.size()]);
070      this.queryArgs = b.queryArgs.toArray(new RemoteOperationArg[b.queryArgs.size()]);
071      this.formDataArgs = b.formDataArgs.toArray(new RemoteOperationArg[b.formDataArgs.size()]);
072      this.headerArgs = b.headerArgs.toArray(new RemoteOperationArg[b.headerArgs.size()]);
073      this.requestArgs = b.requestArgs.toArray(new RemoteOperationBeanArg[b.requestArgs.size()]);
074      this.contentArg = b.bodyArg;
075      this.methodReturn = b.methodReturn;
076      this.exceptions = m.getExceptionTypes();
077   }
078
079   private static class Builder {
080      String httpMethod, fullPath, path;
081      List<RemoteOperationArg>
082         pathArgs = new LinkedList<>(),
083         queryArgs = new LinkedList<>(),
084         headerArgs = new LinkedList<>(),
085         formDataArgs = new LinkedList<>();
086      List<RemoteOperationBeanArg>
087         requestArgs = new LinkedList<>();
088      RemoteOperationArg bodyArg;
089      RemoteOperationReturn methodReturn;
090
091      Builder(String parentPath, Method m, String defaultMethod) {
092
093         MethodInfo mi = MethodInfo.of(m);
094
095         AnnotationList al = mi.getAnnotationList(REMOTE_OP_GROUP);
096         if (al.isEmpty())
097            al = mi.getReturnType().unwrap(Value.class,Optional.class).getAnnotationList(REMOTE_OP_GROUP);
098
099         Value<String> _httpMethod = Value.empty(), _path = Value.empty();
100         al.stream().map(x -> x.getName().substring(6).toUpperCase()).filter(x -> ! x.equals("OP")).forEach(x -> _httpMethod.set(x));
101         al.forEachValue(String.class, "method", NOT_EMPTY, x -> _httpMethod.set(x.trim().toUpperCase()));
102         al.forEachValue(String.class, "path", NOT_EMPTY, x-> _path.set(x.trim()));
103         httpMethod = _httpMethod.orElse("").trim();
104         path = _path.orElse("").trim();
105
106         Value<String> value = Value.empty();
107         al.forEach(RemoteOp.class, x -> isNotEmpty(x.inner().value().trim()), x -> value.set(x.inner().value().trim()));
108
109         if (value.isPresent()) {
110            String v = value.get();
111            int i = v.indexOf(' ');
112            if (i == -1) {
113               httpMethod = v;
114            } else {
115               httpMethod = v.substring(0, i).trim();
116               path = v.substring(i).trim();
117            }
118         } else {
119            al.forEach(x -> ! x.isType(RemoteOp.class) && isNotEmpty(x.getValue(String.class, "value", NOT_EMPTY).orElse("").trim()),x -> value.set(x.getValue(String.class, "value", NOT_EMPTY).get().trim()));
120            if (value.isPresent())
121               path = value.get();
122         }
123
124         if (path.isEmpty()) {
125            path = HttpUtils.detectHttpPath(m, Utils.nullIfEmpty3(httpMethod));
126         }
127         if (httpMethod.isEmpty())
128            httpMethod = HttpUtils.detectHttpMethod(m, true, defaultMethod);
129
130         path = trimSlashes(path);
131
132         if (! isOneOf(httpMethod, "DELETE", "GET", "POST", "PUT", "OPTIONS", "HEAD", "CONNECT", "TRACE", "PATCH"))
133            throw new RemoteMetadataException(m,
134               "Invalid value specified for @RemoteOp(httpMethod) annotation: '"+httpMethod+"'.  Valid values are [DELETE,GET,POST,PUT,OPTIONS,HEAD,CONNECT,TRACE,PATCH].");
135
136         methodReturn = new RemoteOperationReturn(mi);
137
138         fullPath = path.indexOf("://") != -1 ? path : (parentPath.isEmpty() ? urlEncodePath(path) : (trimSlashes(parentPath) + '/' + urlEncodePath(path)));
139
140         mi.getParams().forEach(x -> {
141            RemoteOperationArg rma = RemoteOperationArg.create(x);
142            if (rma != null) {
143               HttpPartType pt = rma.getPartType();
144               if (pt == HEADER)
145                  headerArgs.add(rma);
146               else if (pt == QUERY)
147                  queryArgs.add(rma);
148               else if (pt == FORMDATA)
149                  formDataArgs.add(rma);
150               else if (pt == PATH)
151                  pathArgs.add(rma);
152               else
153                  bodyArg = rma;
154            }
155            RequestBeanMeta rmba = RequestBeanMeta.create(x, AnnotationWorkList.create());
156            if (rmba != null) {
157               requestArgs.add(new RemoteOperationBeanArg(x.getIndex(), rmba));
158            }
159         });
160      }
161   }
162
163   /**
164    * Returns the value of the {@link RemoteOp#method() @RemoteOp(method)} annotation on this Java method.
165    *
166    * @return The value of the annotation, never <jk>null</jk>.
167    */
168   public String getHttpMethod() {
169      return httpMethod;
170   }
171
172   /**
173    * Returns the absolute URI of the REST interface invoked by this Java method.
174    *
175    * @return The absolute URI of the REST interface, never <jk>null</jk>.
176    */
177   public String getFullPath() {
178      return fullPath;
179   }
180
181   /**
182    * Performs an action on the {@link Path @Path} annotated arguments on this Java method.
183    *
184    * @param action The action to perform.
185    * @return This object.
186    */
187   public RemoteOperationMeta forEachPathArg(Consumer<RemoteOperationArg> action) {
188      for (RemoteOperationArg a : pathArgs)
189         action.accept(a);
190      return this;
191   }
192
193   /**
194    * Performs an action on the {@link Query @Query} annotated arguments on this Java method.
195    *
196    * @param action The action to perform.
197    * @return This object.
198    */
199   public RemoteOperationMeta forEachQueryArg(Consumer<RemoteOperationArg> action) {
200      for (RemoteOperationArg a : queryArgs)
201         action.accept(a);
202      return this;
203   }
204
205   /**
206    * Performs an action on the {@link FormData @FormData} annotated arguments on this Java method.
207    *
208    * @param action The action to perform.
209    * @return This object.
210    */
211   public RemoteOperationMeta forEachFormDataArg(Consumer<RemoteOperationArg> action) {
212      for (RemoteOperationArg a : formDataArgs)
213         action.accept(a);
214      return this;
215   }
216
217   /**
218    * Performs an action on the {@link Header @Header} annotated arguments on this Java method.
219    *
220    * @param action The action to perform.
221    * @return This object.
222    */
223   public RemoteOperationMeta forEachHeaderArg(Consumer<RemoteOperationArg> action) {
224      for (RemoteOperationArg a : headerArgs)
225         action.accept(a);
226      return this;
227   }
228
229   /**
230    * Performs an action on the {@link Request @Request} annotated arguments on this Java method.
231    *
232    * @param action The action to perform.
233    * @return This object.
234    */
235   public RemoteOperationMeta forEachRequestArg(Consumer<RemoteOperationBeanArg> action) {
236      for (RemoteOperationBeanArg a : requestArgs)
237         action.accept(a);
238      return this;
239   }
240
241   /**
242    * Returns the argument annotated with {@link Content @Content}.
243    *
244    * @return A index of the argument with the {@link Content @Content} annotation, or <jk>null</jk> if no argument exists.
245    */
246   public RemoteOperationArg getContentArg() {
247      return contentArg;
248   }
249
250   /**
251    * Returns whether the method returns the HTTP response body or status code.
252    *
253    * @return Whether the method returns the HTTP response body or status code.
254    */
255   public RemoteOperationReturn getReturns() {
256      return methodReturn;
257   }
258
259   /**
260    * Performs an action on the exceptions thrown by this method.
261    *
262    * @param action The action to perform.
263    * @return This object.
264    */
265   public RemoteOperationMeta forEachException(Consumer<Class<?>> action) {
266      for (Class<?> e : exceptions)
267         action.accept(e);
268      return this;
269   }
270}