001/*
002 * Copyright (c) 2010-2024 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.batch;
023
024import static com.restfb.util.UrlUtils.urlEncode;
025import static java.lang.String.format;
026import static java.util.Arrays.asList;
027import static java.util.Collections.unmodifiableList;
028
029import java.util.ArrayList;
030import java.util.List;
031import java.util.Optional;
032import java.util.stream.Collectors;
033
034import com.restfb.Facebook;
035import com.restfb.Parameter;
036import com.restfb.util.ReflectionUtils;
037
038/**
039 * Encapsulates a discrete part of an entire
040 * <a href="https://developers.facebook.com/docs/reference/api/batch/" target="_blank">Facebook Batch API</a> request.
041 * <p>
042 * Must be constructed by {@link BatchRequestBuilder}.
043 * 
044 * @author <a href="http://restfb.com">Mark Allen</a>
045 * @since 1.6.5
046 * @see BatchRequestBuilder
047 */
048public class BatchRequest {
049  @Facebook
050  private String method;
051
052  @Facebook("relative_url")
053  private String relativeUrl;
054
055  @Facebook
056  private String body;
057
058  @Facebook("attached_files")
059  private String attachedFiles;
060
061  @Facebook("depends_on")
062  private String dependsOn;
063
064  @Facebook
065  private String name;
066
067  @Facebook("omit_response_on_success")
068  private boolean omitResponseOnSuccess;
069
070  @Facebook
071  private List<BatchHeader> headers;
072
073  /**
074   * Designed to be invoked by instances of <tt>{@link BatchRequestBuilder}</tt> .
075   * 
076   * @param relativeUrl
077   *          The endpoint to hit, for example {@code "me/friends"}.
078   * @param parameters
079   *          Optional list of URL parameters to be added to the value specified in {@code relativeUrl}.
080   * @param method
081   *          The HTTP method to use, for example {@code "GET"}.
082   * @param headers
083   *          The list of HTTP headers for the request.
084   * @param bodyParameters
085   *          The parameters that comprise the request body, for example {@code "message=Test status update"} .
086   * @param attachedFiles
087   *          Names of any attached files for this call, for example {@code "cat1, cat2"}.
088   * @param name
089   *          The logical name of this request, for example {@code "get-friends"}.
090   * @param dependsOn
091   *          If this call depends on the completion of another call in the current batch, for example
092   *          {@code "get-friends"}.
093   * @param omitResponseOnSuccess
094   *          To make sure FB returns JSON in the event that this request completes successfully, set this to
095   *          {@code false}.
096   * @throws IllegalArgumentException
097   *           If {@code relativeUrl} is {@code null}.
098   */
099  protected BatchRequest(String relativeUrl, List<Parameter> parameters, String method, List<BatchHeader> headers,
100      List<Parameter> bodyParameters, String attachedFiles, String dependsOn, String name,
101      boolean omitResponseOnSuccess) {
102
103    this.relativeUrl = Optional.ofNullable(relativeUrl)
104      .orElseThrow(() -> new IllegalArgumentException("The 'relativeUrl' parameter is required."));
105    this.method = method;
106    this.headers = headers;
107    this.attachedFiles = attachedFiles;
108    this.dependsOn = dependsOn;
109    this.name = name;
110    this.omitResponseOnSuccess = omitResponseOnSuccess;
111
112    if (!parameters.isEmpty()) {
113      this.relativeUrl = format(!this.relativeUrl.contains("?") ? "%s?%s" : "%s&%s", this.relativeUrl,
114        generateParameterString(parameters));
115    }
116
117    this.body = generateParameterString(bodyParameters);
118  }
119
120  /**
121   * Builder pattern implementation used to construct instances of <tt>{@link BatchRequest}</tt>.
122   * <p>
123   * See the <a href="https://developers.facebook.com/docs/reference/api/batch/" target="_blank">Facebook Batch API
124   * documentation</a> for more details on what a batch request looks like.
125   * 
126   * @author <a href="http://restfb.com">Mark Allen</a>
127   * @since 1.6.5
128   */
129  public static class BatchRequestBuilder {
130    private String method = "GET";
131    private String relativeUrl;
132    private List<Parameter> parameters = new ArrayList<>();
133    private List<BatchHeader> headers = new ArrayList<>();
134    private List<Parameter> bodyParameters = new ArrayList<>();
135    private String attachedFiles;
136    private String dependsOn;
137    private String name;
138    private boolean omitResponseOnSuccess;
139
140    /**
141     * Creates a batch request builder using the provided FB endpoint.
142     * <p>
143     * You can explicitly specify URL parameters here, or use {@link #parameters(Parameter...)} instead if you prefer to
144     * have the query string constructed programmatically.
145     * 
146     * @param relativeUrl
147     *          The endpoint to hit, for example {@code "me/friends"}.
148     */
149    public BatchRequestBuilder(String relativeUrl) {
150      this.relativeUrl = relativeUrl;
151    }
152
153    /**
154     * Sets the HTTP method for the request generated by this builder, for example {@code "POST"} ({@code GET} is the
155     * default value for this builder).
156     * 
157     * @param method
158     *          The HTTP method.
159     * @return This builder.
160     */
161    public BatchRequestBuilder method(String method) {
162      this.method = method;
163      return this;
164    }
165
166    /**
167     * Sets the logical name for the request generated by this builder. Useful for specifying dependencies between
168     * operations - the generated request can be referenced by name.
169     * 
170     * @param name
171     *          The logical name of the request generated by this builder.
172     * @return This builder.
173     */
174    public BatchRequestBuilder name(String name) {
175      this.name = name;
176      return this;
177    }
178
179    /**
180     * Sets the list of HTTP headers for the request generated by this builder.
181     * 
182     * @param headers
183     *          The HTTP headers.
184     * @return This builder.
185     */
186    public BatchRequestBuilder headers(BatchHeader... headers) {
187      this.headers.clear();
188      this.headers.addAll(asList(headers));
189      return this;
190    }
191
192    /**
193     * Sets the request body parameters for the request generated by this builder, for example
194     * {@code Parameter.with("message", "Test status update")}.
195     * 
196     * @param parameters
197     *          The request body parameters.
198     * @return This builder.
199     */
200    public BatchRequestBuilder body(Parameter... parameters) {
201      this.bodyParameters.clear();
202      this.bodyParameters.addAll(asList(parameters));
203      return this;
204    }
205
206    /**
207     * Sets the comma-delimited names of any attached files for this builder, for example {@code "cat1, cat2"}.
208     * 
209     * @param attachedFiles
210     *          The names of any attached files for this builder.
211     * @return This builder.
212     */
213    public BatchRequestBuilder attachedFiles(String attachedFiles) {
214      this.attachedFiles = attachedFiles;
215      return this;
216    }
217
218    /**
219     * Specifies if the request generated by this builder depends on the completion of another call in the current
220     * batch, for example {@code "first"}.
221     * 
222     * @param dependsOn
223     *          A reference to another request in the batch that this builder's request depends on.
224     * @return This builder.
225     */
226    public BatchRequestBuilder dependsOn(String dependsOn) {
227      this.dependsOn = dependsOn;
228      return this;
229    }
230
231    /**
232     * To make sure FB returns JSON in the event that this builder's request completes successfully, set this to
233     * {@code false}.
234     * 
235     * @param omitResponseOnSuccess
236     *          Set this to {@code false} to make sure FB returns JSON in the event that this builder's request
237     *          completes successfully,
238     * @return This builder.
239     */
240    public BatchRequestBuilder omitResponseOnSuccess(boolean omitResponseOnSuccess) {
241      this.omitResponseOnSuccess = omitResponseOnSuccess;
242      return this;
243    }
244
245    /**
246     * Specifies URL parameters for the request generated by this builder.
247     * 
248     * @param parameters
249     *          The URL parameters.
250     * @return This builder.
251     */
252    public BatchRequestBuilder parameters(Parameter... parameters) {
253      this.parameters.clear();
254      this.parameters.addAll(asList(parameters));
255      return this;
256    }
257
258    /**
259     * Generates an instance of {@link BatchRequest}.
260     * 
261     * @return An instance of {@link BatchRequest}.
262     */
263    public BatchRequest build() {
264      return new BatchRequest(relativeUrl, parameters, method, headers, bodyParameters, attachedFiles, dependsOn, name,
265        omitResponseOnSuccess);
266    }
267  }
268
269  /**
270   * For a list of parameters, generate a URL query string.
271   * <p>
272   * Does not include a leading "?" character.
273   * 
274   * @param parameters
275   *          The parameters to stringify.
276   * @return A URL query string representation of the given {@code parameters}.
277   */
278  protected String generateParameterString(List<Parameter> parameters) {
279    if (parameters == null) {
280      return "";
281    }
282
283    return parameters.stream().map(p -> urlEncode(p.name) + "=" + urlEncode(p.value)).collect(Collectors.joining("&"));
284  }
285
286  @Override
287  public int hashCode() {
288    return ReflectionUtils.hashCode(this);
289  }
290
291  @Override
292  public boolean equals(Object that) {
293    return ReflectionUtils.equals(this, that);
294  }
295
296  @Override
297  public String toString() {
298    return ReflectionUtils.toString(this);
299  }
300
301  /**
302   * The HTTP method to use, for example {@code "GET"}.
303   * 
304   * @return The HTTP method to use.
305   */
306  public String getMethod() {
307    return method;
308  }
309
310  /**
311   * The endpoint to hit, for example {@code "me/friends?limit=10"}.
312   * 
313   * @return The endpoint to hit.
314   */
315  public String getRelativeUrl() {
316    return relativeUrl;
317  }
318
319  /**
320   * The request body, for example {@code "message=Test status update"}.
321   * 
322   * @return The request body.
323   */
324  public String getBody() {
325    return body;
326  }
327
328  /**
329   * Names of any attached files for this call, for example {@code "cat1, cat2"} .
330   * 
331   * @return Names of any attached files for this call.
332   */
333  public String getAttachedFiles() {
334    return attachedFiles;
335  }
336
337  /**
338   * The logical name for this request, for example {@code "get-friends"}.
339   * 
340   * @return The logical name for this request.
341   */
342  public String getName() {
343    return name;
344  }
345
346  /**
347   * Another call in the current batch upon which this call depends, for example {@code "get-friends"}.
348   * 
349   * @return Another call in the current batch upon which this call depends.
350   */
351  public String getDependsOn() {
352    return dependsOn;
353  }
354
355  /**
356   * Will the batch response for this request be {@code null}?
357   * 
358   * @return {@code true} if the batch response for this request will be {@code null}, {@code false} otherwise.
359   */
360  public boolean isOmitResponseOnSuccess() {
361    return omitResponseOnSuccess;
362  }
363
364  /**
365   * HTTP Headers to be sent as part of this request.
366   * 
367   * @return HTTP Headers to be sent as part of this request.
368   */
369  public List<BatchHeader> getHeaders() {
370    return unmodifiableList(headers);
371  }
372}