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}