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.utils.ClassUtils.*; 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.classic.HttpHeaders.*; 024 025import java.lang.reflect.*; 026import java.util.*; 027 028import org.apache.juneau.commons.reflect.*; 029import org.apache.juneau.commons.svl.*; 030import org.apache.juneau.http.classic.header.*; 031import org.apache.juneau.http.remote.*; 032 033/** 034 * Contains the meta-data about a REST proxy class. 035 * 036 * <p> 037 * Captures the information in {@link Remote @Remote} and {@link RemoteOp @RemoteOp} annotations for 038 * caching and reuse. 039 * 040 * <h5 class='section'>See Also:</h5><ul> 041 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestProxies">REST Proxies</a> 042 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestClient">juneau-rest-client-classic</a> 043 * </ul> 044 */ 045public class RemoteMeta { 046 047 private static String resolve(String s) { 048 return VarResolver.DEFAULT.resolve(s); 049 } 050 051 private final Map<Method,RemoteOperationMeta> operations; 052 053 private final HeaderList headers; 054 055 // Interface-level members honored for classic/NG parity (B-client-1). 056 private final String baseUrl; 057 private final String accept; 058 private final String contentType; 059 private final List<Map.Entry<String,String>> queryData; 060 private final List<Map.Entry<String,String>> formData; 061 private final String timeout; 062 private final int retries; 063 private final boolean retryNonIdempotent; 064 private final boolean throwOnError; 065 private final boolean allowPrivateUrls; 066 067 /** 068 * Constructor. 069 * 070 * @param c The interface class annotated with a {@link Remote @Remote} annotation (optional). 071 */ 072 @SuppressWarnings({ 073 "java:S3776" // Cognitive complexity acceptable for remote interface metadata construction dispatch 074 }) 075 public RemoteMeta(Class<?> c) { 076 var path = ""; 077 078 var ci = ClassInfo.of(c); 079 var remotes = rstream(ci.getAnnotations(Remote.class).toList()).map(AnnotationInfo::inner).toList(); 080 081 var versionHeader = "Client-Version"; 082 String clientVersion = null; 083 var headers2 = HeaderList.create().resolving(); 084 085 var baseUrl2 = ""; 086 var accept2 = ""; 087 var contentType2 = ""; 088 var queryData2 = new ArrayList<Map.Entry<String,String>>(); 089 var formData2 = new ArrayList<Map.Entry<String,String>>(); 090 var timeout2 = ""; 091 var retries2 = 0; 092 var retryNonIdempotent2 = false; 093 var throwOnError2 = false; 094 var allowPrivateUrls2 = false; 095 096 for (var r : remotes) { 097 if (ine(r.path())) 098 path = trimSlashes(resolve(r.path())); 099 else if (ine(r.value())) 100 path = trimSlashes(resolve(r.value())); 101 for (var h : r.headers()) 102 headers2.append(stringHeader(resolve(h))); 103 if (ine(r.version())) 104 clientVersion = resolve(r.version()); 105 if (ine(r.versionHeader())) 106 versionHeader = resolve(r.versionHeader()); 107 if (isNotVoid(r.headerList()) && HeaderList.class.isAssignableFrom(r.headerList())) { 108 try { 109 headers2.append(((HeaderList) r.headerList().getDeclaredConstructor().newInstance()).getAll()); 110 } catch (Exception e) { 111 throw rex(e, "Could not instantiate HeaderSupplier class"); 112 } 113 } 114 if (ine(r.baseUrl())) 115 baseUrl2 = resolve(r.baseUrl()); 116 if (ine(r.accept())) 117 accept2 = resolve(r.accept()); 118 if (ine(r.contentType())) 119 contentType2 = resolve(r.contentType()); 120 queryData2.addAll(RemoteProxyUtils.parseConstantParts(r.queryData(), '=')); 121 formData2.addAll(RemoteProxyUtils.parseConstantParts(r.formData(), '=')); 122 if (ine(r.timeout())) 123 timeout2 = r.timeout(); 124 if (r.retries() > 0) 125 retries2 = r.retries(); 126 retryNonIdempotent2 |= r.retryNonIdempotent(); 127 throwOnError2 |= r.throwOnError(); 128 allowPrivateUrls2 |= r.allowPrivateUrls(); 129 // Genuinely engine-specific: classic and NG RestCallInterceptor SPI types are nominally 130 // incompatible, so the classic engine cannot honor interface-level interceptors. Warn once. 131 if (r.interceptors().length > 0) 132 RemoteProxyUtils.warnUnsupportedMember(c, "interceptors", 133 "classic and next-generation RestCallInterceptor SPI types are nominally incompatible."); 134 } 135 136 if (nn(clientVersion)) 137 headers2.append(stringHeader(versionHeader, clientVersion)); 138 139 Map<Method,RemoteOperationMeta> operations2 = map(); 140 var path2 = path; 141 ci.getPublicMethods().forEach(x -> operations2.put(x.inner(), new RemoteOperationMeta(path2, x.inner(), "GET"))); 142 143 this.operations = u(operations2); 144 this.headers = headers2.unmodifiable(); 145 this.baseUrl = baseUrl2; 146 this.accept = accept2; 147 this.contentType = contentType2; 148 this.queryData = u(queryData2); 149 this.formData = u(formData2); 150 this.timeout = timeout2; 151 this.retries = retries2; 152 this.retryNonIdempotent = retryNonIdempotent2; 153 this.throwOnError = throwOnError2; 154 this.allowPrivateUrls = allowPrivateUrls2; 155 } 156 157 /** 158 * Returns the headers to set on all requests. 159 * 160 * @return The headers to set on all requests. 161 */ 162 public HeaderList getHeaders() { return headers; } 163 164 /** 165 * Returns the interface-level base/host override from {@link Remote#baseUrl()}. 166 * 167 * @return The base/host override. Never <jk>null</jk>, but may be empty. 168 */ 169 public String getBaseUrl() { return baseUrl; } 170 171 /** 172 * Returns the interface-level default {@code Accept} media type from {@link Remote#accept()}. 173 * 174 * @return The accept media type. Never <jk>null</jk>, but may be empty. 175 */ 176 public String getAccept() { return accept; } 177 178 /** 179 * Returns the interface-level default {@code Content-Type} media type from {@link Remote#contentType()}. 180 * 181 * @return The content-type media type. Never <jk>null</jk>, but may be empty. 182 */ 183 public String getContentType() { return contentType; } 184 185 /** 186 * Returns the interface-level constant query parameters from {@link Remote#queryData()}. 187 * 188 * @return An unmodifiable list of name/value entries. Never <jk>null</jk>, but may be empty. 189 */ 190 public List<Map.Entry<String,String>> getQueryData() { return queryData; } 191 192 /** 193 * Returns the interface-level constant form-data parameters from {@link Remote#formData()}. 194 * 195 * @return An unmodifiable list of name/value entries. Never <jk>null</jk>, but may be empty. 196 */ 197 public List<Map.Entry<String,String>> getFormData() { return formData; } 198 199 /** 200 * Returns the interface-level default per-call timeout duration string from {@link Remote#timeout()}. 201 * 202 * @return The timeout duration string. Never <jk>null</jk>, but may be empty. 203 */ 204 public String getTimeout() { return timeout; } 205 206 /** 207 * Returns the interface-level default maximum retry attempts from {@link Remote#retries()}. 208 * 209 * @return The retry count. 210 */ 211 public int getRetries() { return retries; } 212 213 /** 214 * Returns whether the interface opts non-idempotent verbs into automatic retries ({@link Remote#retryNonIdempotent()}). 215 * 216 * @return <jk>true</jk> if non-idempotent retries are opted in at the interface level. 217 */ 218 public boolean isRetryNonIdempotent() { return retryNonIdempotent; } 219 220 /** 221 * Returns whether the interface throws a generic exception on an unmatched error response ({@link Remote#throwOnError()}). 222 * 223 * @return <jk>true</jk> if {@code throwOnError} is set at the interface level. 224 */ 225 public boolean isThrowOnError() { return throwOnError; } 226 227 /** 228 * Returns whether the interface opts its absolute {@code @Remote}/{@code @Url}/{@code baseUrl()} targets out of 229 * the default deny-private SSRF guardrail ({@link Remote#allowPrivateUrls()}). 230 * 231 * @return <jk>true</jk> if {@code allowPrivateUrls} is set at the interface level. 232 */ 233 public boolean isAllowPrivateUrls() { return allowPrivateUrls; } 234 235 /** 236 * Returns the metadata about the specified operation on this resource proxy. 237 * 238 * @param m The method to look up. 239 * @return Metadata about the method or <jk>null</jk> if no metadata was found. 240 */ 241 public RemoteOperationMeta getOperationMeta(Method m) { 242 return operations.get(m); 243 } 244}