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}