001/* 002 * Copyright (c) 2010-2026 Mark Allen, Norbert Bartels. 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.restfb; 023 024import static com.restfb.util.StringUtils.isBlank; 025import static com.restfb.util.StringUtils.trimToEmpty; 026import static java.lang.String.format; 027 028import java.io.IOException; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collections; 032import java.util.List; 033import java.util.Map; 034import java.util.Optional; 035import java.util.concurrent.atomic.AtomicReference; 036import java.util.function.BiConsumer; 037 038import com.restfb.types.FacebookReelAttachment; 039import com.restfb.util.StringUtils; 040 041/** 042 * Specifies how a class that sends {@code HTTP} requests to the Facebook API endpoint must operate. 043 * 044 * @author <a href="http://restfb.com">Mark Allen</a> 045 */ 046public interface WebRequestor { 047 /** 048 * Encapsulates an HTTP response body and status code. 049 * 050 * @author <a href="http://restfb.com">Mark Allen</a> 051 */ 052 class Response { 053 /** 054 * HTTP response status code (e.g. 200). 055 */ 056 private final Integer statusCode; 057 058 /** 059 * HTTP response body as text. 060 */ 061 private final String body; 062 063 /** 064 * HTTP headers returned by Facebook. 065 */ 066 private final Map<String, List<String>> headers; 067 068 private final AtomicReference<Optional<DebugHeaderInfo>> debugHeaderInfoRef = new AtomicReference<>(); 069 070 /** 071 * Creates a response with the given HTTP status code and response body as text. 072 * 073 * @param statusCode 074 * The HTTP status code of the response. 075 * @param body 076 * The response body as text. 077 */ 078 public Response(Integer statusCode, String body) { 079 this(statusCode, body, null, null); 080 } 081 082 /** 083 * Creates a response with the given HTTP status code, response body and debug header info. 084 * 085 * @param statusCode 086 * The HTTP status code of the response. 087 * @param body 088 * The response body as text. 089 * @param debugHeaderInfo 090 * debug info parsed from the headers (may be {@code null}) 091 */ 092 public Response(Integer statusCode, String body, DebugHeaderInfo debugHeaderInfo) { 093 this(statusCode, body, debugHeaderInfo, null); 094 } 095 096 /** 097 * Creates a response with status, body, debug info and headers. 098 */ 099 public Response(Integer statusCode, String body, DebugHeaderInfo debugHeaderInfo, 100 Map<String, List<String>> headers) { 101 this.statusCode = statusCode; 102 this.body = trimToEmpty(body); 103 this.headers = headers == null ? null : Collections.unmodifiableMap(headers); 104 if (debugHeaderInfo != null) { 105 this.debugHeaderInfoRef.set(Optional.of(debugHeaderInfo)); 106 } 107 } 108 109 /** 110 * Gets the HTTP status code. 111 * 112 * @return The HTTP status code. 113 */ 114 public Integer getStatusCode() { 115 return statusCode; 116 } 117 118 /** 119 * Gets the HTTP response body as text. 120 * 121 * @return The HTTP response body as text. 122 */ 123 public String getBody() { 124 return body; 125 } 126 127 /** 128 * Gets the debug header information parsed from the response headers. 129 * 130 * @return debug header information, may be {@code null} 131 */ 132 public DebugHeaderInfo getDebugHeaderInfo() { 133 Optional<DebugHeaderInfo> cached = debugHeaderInfoRef.get(); 134 if (cached != null) { 135 return cached.orElse(null); 136 } 137 138 if (headers == null || headers.isEmpty()) { 139 return null; 140 } 141 142 DebugHeaderInfo built = buildDebugHeaderInfo(headers); 143 Optional<DebugHeaderInfo> optional = Optional.ofNullable(built); 144 if (!debugHeaderInfoRef.compareAndSet(null, optional)) { 145 return debugHeaderInfoRef.get().orElse(null); 146 } 147 return optional.orElse(null); 148 } 149 150 /** 151 * Gets the HTTP headers returned by Facebook. 152 * 153 * @return unmodifiable header map or {@code null} 154 */ 155 public Map<String, List<String>> getHeaders() { 156 return headers; 157 } 158 159 private DebugHeaderInfo buildDebugHeaderInfo(Map<String, List<String>> responseHeaders) { 160 String usedApiVersion = StringUtils.trimToEmpty(getHeaderValue(responseHeaders, "facebook-api-version")); 161 Version usedVersion = Version.getVersionFromString(usedApiVersion); 162 DebugHeaderInfo.DebugHeaderInfoFactory factory = 163 DebugHeaderInfo.DebugHeaderInfoFactory.create().setVersion(usedVersion); 164 165 Arrays.stream(FbHeaderField.values()).forEach(f -> f.apply(responseHeaders, factory)); 166 return factory.build(); 167 } 168 169 private static String getHeaderValue(Map<String, List<String>> responseHeaders, String fieldName) { 170 if (responseHeaders == null) { 171 return ""; 172 } 173 for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) { 174 if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(fieldName)) { 175 List<String> values = entry.getValue(); 176 if (values.isEmpty()) { 177 return ""; 178 } 179 return values.get(0); 180 } 181 } 182 return ""; 183 } 184 185 private enum FbHeaderField { 186 X_FB_TRACE_ID("x-fb-trace-id", DebugHeaderInfo.DebugHeaderInfoFactory::setTraceId), // 187 X_FB_REV("x-fb-rev", DebugHeaderInfo.DebugHeaderInfoFactory::setRev), // 188 X_FB_DEBUG("x-fb-debug", DebugHeaderInfo.DebugHeaderInfoFactory::setDebug), // 189 X_APP_USAGE("x-app-usage", DebugHeaderInfo.DebugHeaderInfoFactory::setAppUsage), // 190 X_PAGE_USAGE("x-page-usage", DebugHeaderInfo.DebugHeaderInfoFactory::setPageUsage), // 191 X_AD_ACCOUNT_USAGE("x-ad-account-usage", DebugHeaderInfo.DebugHeaderInfoFactory::setAdAccountUsage), // 192 X_BUSINESS_USE_CASE_USAGE("x-business-use-case-usage", 193 DebugHeaderInfo.DebugHeaderInfoFactory::setBusinessUseCaseUsage); 194 195 private final String headerName; 196 private final BiConsumer<DebugHeaderInfo.DebugHeaderInfoFactory, String> consumer; 197 198 FbHeaderField(String headerName, BiConsumer<DebugHeaderInfo.DebugHeaderInfoFactory, String> consumer) { 199 this.headerName = headerName; 200 this.consumer = consumer; 201 } 202 203 void apply(Map<String, List<String>> responseHeaders, DebugHeaderInfo.DebugHeaderInfoFactory factory) { 204 consumer.accept(factory, StringUtils.trimToEmpty(getHeaderValue(responseHeaders, headerName))); 205 } 206 } 207 208 /** 209 * @see java.lang.Object#toString() 210 */ 211 @Override 212 public String toString() { 213 if (isBlank(getBody())) { 214 return format("HTTP status code %d and an empty response body.", getStatusCode()); 215 } 216 return format("HTTP status code %d and response body: %s", getStatusCode(), getBody()); 217 } 218 } 219 220 /** 221 * encapsulates the HTTP Request configuration 222 */ 223 class Request { 224 225 private final String url; 226 227 private final Optional<String> headerAccessToken; 228 229 private String parameters; 230 231 private Body body; 232 233 private List<BinaryAttachment> binaryAttachments; 234 235 /** 236 * Simple http request with url and a header access token 237 * 238 * @param url 239 * the endpoint the request ist directed to 240 * @param headerAccessToken 241 * the HTTP header access token (may be {@code null}) 242 */ 243 public Request(String url, String headerAccessToken) { 244 this(url, headerAccessToken, null); 245 } 246 247 /** 248 * Simple http request with url and a header access token 249 * 250 * @param url 251 * the endpoint the request ist directed to 252 * @param headerAccessToken 253 * the HTTP header access token (may be {@code null}) 254 * @param parameters 255 * the query parameter string 256 */ 257 public Request(String url, String headerAccessToken, String parameters) { 258 this(url, headerAccessToken, parameters, null); 259 } 260 261 /** 262 * Simple http request with url and a header access token 263 * 264 * @param url 265 * the endpoint the request ist directed to 266 * @param headerAccessToken 267 * the HTTP header access token (may be {@code null}) 268 * @param parameters 269 * the query parameter string 270 * @param attachments 271 * list of binary attachments 272 */ 273 public Request(String url, String headerAccessToken, String parameters, List<BinaryAttachment> attachments) { 274 this.url = url; 275 this.headerAccessToken = Optional.ofNullable(headerAccessToken); 276 this.parameters = parameters; 277 setBinaryAttachments(attachments); 278 } 279 280 public String getUrl() { 281 return url; 282 } 283 284 public String getHeaderAccessToken() { 285 return headerAccessToken.orElse(null); 286 } 287 288 public boolean hasHeaderAccessToken() { 289 return headerAccessToken.isPresent(); 290 } 291 292 public String getParameters() { 293 return parameters; 294 } 295 296 public List<BinaryAttachment> getBinaryAttachments() { 297 return Optional.ofNullable(binaryAttachments).orElse(new ArrayList<>()); 298 } 299 300 public void setBinaryAttachments(List<BinaryAttachment> binaryAttachments) { 301 this.binaryAttachments = Optional.ofNullable(binaryAttachments).orElse(new ArrayList<>()); 302 } 303 304 public String getFullUrl() { 305 if (!StringUtils.isBlank(parameters)) { 306 if (url != null && url.contains("?")) { 307 return url + "&" + parameters; 308 } 309 return url + "?" + parameters; 310 } 311 return url; 312 } 313 314 @Override 315 public String toString() { 316 return format("Request to url %s with parameters %s. Header access token: %b", getUrl(), getParameters(), 317 hasHeaderAccessToken()); 318 } 319 320 public void setBody(Body body) { 321 this.body = body; 322 } 323 324 public Body getBody() { 325 return body; 326 } 327 328 public boolean hasBody() { 329 return body != null; 330 } 331 332 public boolean isReelUpload() { 333 List<BinaryAttachment> attachments = getBinaryAttachments(); 334 if (attachments.size() == 1) { 335 return attachments.get(0).isFacebookReel(); 336 } 337 338 return false; 339 } 340 341 public Optional<FacebookReelAttachment> getReel() { 342 if (isReelUpload()) { 343 return Optional.of((FacebookReelAttachment) getBinaryAttachments().get(0)); 344 } 345 346 return Optional.empty(); 347 } 348 } 349 350 /** 351 * Given a Facebook API endpoint URL, execute a {@code GET} against it. 352 * 353 * @param request 354 * The request data for the {@code GET} request 355 * @return HTTP response data. 356 * @throws IOException 357 * If an error occurs while performing the {@code GET} operation. 358 * @since 1.5 359 */ 360 Response executeGet(Request request) throws IOException; 361 362 /** 363 * Given a Facebook API endpoint URL and parameter string, execute a {@code POST} to the endpoint URL. 364 * 365 * @param request 366 * The request data used for the {@code POST} request. 367 * @return HTTP response data. 368 * @throws IOException 369 * If an error occurs while performing the {@code POST}. 370 */ 371 Response executePost(Request request) throws IOException; 372 373 /** 374 * Given a Facebook API endpoint URL and parameter string, execute a {@code DELETE} to the endpoint URL. 375 * 376 * @param request 377 * The request data used for the {@code DELETE} request. 378 * @return HTTP response data. 379 * @throws IOException 380 * If an error occurs while performing the {@code DELETE}. 381 */ 382 Response executeDelete(Request request) throws IOException; 383 384}