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 java.util.Collections.unmodifiableList;
026
027import java.util.List;
028import java.util.NoSuchElementException;
029import java.util.Optional;
030import java.util.stream.Collectors;
031
032import com.restfb.exception.FacebookJsonMappingException;
033import com.restfb.json.Json;
034import com.restfb.json.JsonArray;
035import com.restfb.json.JsonObject;
036import com.restfb.json.ParseException;
037import com.restfb.util.ReflectionUtils;
038
039/**
040 * Represents a <a href="http://developers.facebook.com/docs/api">Graph API Connection type</a>.
041 *
042 * @param <T>
043 *          The Facebook type
044 * @author <a href="http://restfb.com">Mark Allen</a>
045 */
046public class Connection<T> implements Iterable<List<T>> {
047  private FacebookClient facebookClient;
048  private Class<T> connectionType;
049  private List<T> data;
050  private String previousPageUrl;
051  private String nextPageUrl;
052  private Long totalCount;
053  private String beforeCursor;
054  private String afterCursor;
055  private String order;
056  private String json;
057  private T typedSummary;
058  private ResponseMetadata responseMetadata;
059
060  /**
061   * @see java.lang.Iterable#iterator()
062   * @since 1.6.7
063   */
064  @Override
065  public ConnectionIterator<T> iterator() {
066    return new Itr<>(this);
067  }
068
069  /**
070   * Iterator over connection pages.
071   *
072   * @author <a href="http://restfb.com">Mark Allen</a>
073   * @since 1.6.7
074   */
075  protected static class Itr<T> implements ConnectionIterator<T> {
076    private Connection<T> connection;
077    private boolean initialPage = true;
078
079    /**
080     * Creates a new iterator over the given {@code connection}.
081     *
082     * @param connection
083     *          The connection over which to iterate.
084     */
085    protected Itr(Connection<T> connection) {
086      this.connection = connection;
087    }
088
089    /**
090     * @see java.util.Iterator#hasNext()
091     */
092    @Override
093    public boolean hasNext() {
094      // Special case: initial page will always have data
095      return initialPage || connection.hasNext();
096    }
097
098    /**
099     * @see java.util.Iterator#next()
100     */
101    @Override
102    public List<T> next() {
103      // Special case: initial page will always have data, return it
104      // immediately.
105      if (initialPage) {
106        initialPage = false;
107        return connection.getData();
108      }
109
110      if (!connection.hasNext()) {
111        throw new NoSuchElementException("There are no more pages in the connection.");
112      }
113
114      connection = connection.fetchNextPage();
115      return connection.getData();
116    }
117
118    /**
119     * @see java.util.Iterator#remove()
120     */
121    @Override
122    public void remove() {
123      throw new UnsupportedOperationException(Itr.class.getSimpleName() + " doesn't support the remove() operation.");
124    }
125
126    /**
127     * @see ConnectionIterator#snapshot()
128     */
129    @Override
130    public Connection<T> snapshot() {
131      return connection;
132    }
133  }
134
135  /**
136   * Creates a connection with the given {@code jsonObject}.
137   *
138   * @param facebookClient
139   *          The {@code FacebookClient} used to fetch additional pages and map data to JSON objects.
140   * @param json
141   *          Raw JSON which must include a {@code data} field that holds a JSON array and optionally a {@code paging}
142   *          field that holds a JSON object with next/previous page URLs.
143   * @param connectionType
144   *          Connection type token.
145   * @throws FacebookJsonMappingException
146   *           If the provided {@code json} is invalid.
147   * @since 1.6.7
148   */
149  @SuppressWarnings("unchecked")
150  public Connection(FacebookClient facebookClient, String json, Class<T> connectionType) {
151    JsonObject jsonObject;
152    this.json = json;
153
154    try {
155      jsonObject = Optional.ofNullable(json).map(j -> Json.parse(j).asObject())
156        .orElseThrow(() -> new FacebookJsonMappingException("You must supply non-null connection JSON."));
157    } catch (ParseException e) {
158      throw new FacebookJsonMappingException("The connection JSON you provided was invalid: " + json, e);
159    }
160
161    // Pull out data
162    if (!jsonObject.contains("data")) {
163      throw new FacebookJsonMappingException(
164        "The connection JSON does not contain a data field, maybe it is no connection");
165    }
166    JsonArray jsonData = jsonObject.get("data").asArray();
167    List<T> dataItem = jsonData.valueStream()
168      .map(jsonValue -> connectionType.equals(JsonObject.class) ? (T) jsonValue
169          : facebookClient.getJsonMapper().toJavaObject(jsonValue.toString(), connectionType))
170      .collect(Collectors.toList());
171
172    // Pull out paging info, if present
173    if (jsonObject.contains("paging")) {
174      JsonObject jsonPaging = jsonObject.get("paging").asObject();
175      previousPageUrl = fixProtocol(jsonPaging.getString("previous", null));
176      nextPageUrl = fixProtocol(jsonPaging.getString("next", null));
177
178      // handle cursors
179      if (jsonPaging.contains("cursors")) {
180        JsonObject jsonCursors = jsonPaging.get("cursors").asObject();
181        beforeCursor = jsonCursors.getString("before", null);
182        afterCursor = jsonCursors.getString("after", null);
183      }
184    } else {
185      previousPageUrl = null;
186      nextPageUrl = null;
187    }
188
189    if (jsonObject.contains("summary")) {
190      JsonObject jsonSummary = jsonObject.get("summary").asObject();
191      totalCount = jsonSummary.contains("total_count") ? jsonSummary.getLong("total_count", 0L) : null;
192      order = jsonSummary.getString("order", "");
193
194      // special handling to fill the typed summary (used by ad insights for example)
195      try {
196        typedSummary = facebookClient.getJsonMapper().toJavaObject(jsonSummary.toString(), connectionType);
197      } catch (FacebookJsonMappingException jme) {
198        // ignore mapping exception here
199      }
200    } else {
201      totalCount = null;
202      order = null;
203    }
204
205    this.data = unmodifiableList(dataItem);
206    this.facebookClient = facebookClient;
207    this.connectionType = connectionType;
208  }
209
210  /**
211   * Fetches the next page of the connection. Designed to be used by {@link Itr}.
212   *
213   * @return The next page of the connection.
214   * @since 1.6.7
215   */
216  protected Connection<T> fetchNextPage() {
217    return facebookClient.fetchConnectionPage(getNextPageUrl(), connectionType);
218  }
219
220  @Override
221  public String toString() {
222    return ReflectionUtils.toString(this);
223  }
224
225  @Override
226  public boolean equals(Object object) {
227    return ReflectionUtils.equals(this, object);
228  }
229
230  @Override
231  public int hashCode() {
232    return ReflectionUtils.hashCode(this);
233  }
234
235  /**
236   * Data for this connection.
237   *
238   * @return Data for this connection.
239   */
240  public List<T> getData() {
241    return data;
242  }
243
244  /**
245   * This connection's "previous page of data" URL.
246   *
247   * @return This connection's "previous page of data" URL, or {@code null} if there is no previous page.
248   * @since 1.5.3
249   */
250  public String getPreviousPageUrl() {
251    return previousPageUrl;
252  }
253
254  /**
255   * This connection's "next page of data" URL.
256   *
257   * @return This connection's "next page of data" URL, or {@code null} if there is no next page.
258   * @since 1.5.3
259   */
260  public String getNextPageUrl() {
261    return nextPageUrl;
262  }
263
264  /**
265   * Does this connection have a previous page of data?
266   *
267   * @return {@code true} if there is a previous page of data for this connection, {@code false} otherwise.
268   */
269  public boolean hasPrevious() {
270    return !isBlank(getPreviousPageUrl());
271  }
272
273  /**
274   * Does this connection have a next page of data?
275   *
276   * @return {@code true} if there is a next page of data for this connection, {@code false} otherwise.
277   */
278  public boolean hasNext() {
279    return !isBlank(getNextPageUrl()) && !getData().isEmpty();
280  }
281
282  /**
283   * provides the total count of elements, if FB provides them (API &ge; v2.0)
284   *
285   * @return the total count of elements if present
286   * @since 1.6.16
287   */
288  public Long getTotalCount() {
289    return totalCount;
290  }
291
292  /**
293   * returns the order of the elements
294   *
295   * @return the order of the elements
296   */
297  public String getOrder() {
298    return order;
299  }
300
301  public String getBeforeCursor() {
302    return beforeCursor;
303  }
304
305  public String getAfterCursor() {
306    return afterCursor;
307  }
308
309  /**
310   * return the typed summary.
311   * <p>
312   * For some connections, there is summary object that contains almost the same fields as the type that is used in the
313   * connection. For example ad insights fill the summary that way (if you use the right query parameter)
314   *
315   * @return the typed summary, may be null
316   */
317  public T getTypedSummary() {
318    return typedSummary;
319  }
320
321  private String fixProtocol(String pageUrl) {
322    return Optional.ofNullable(pageUrl).filter(s -> s.startsWith("http://"))
323      .map(s -> s.replaceFirst("http://", "https://")).orElse(pageUrl);
324  }
325
326  /**
327   * replace the current facebookclient with the new one.
328   *
329   * @param facebookClient
330   *          the new FacebookClient
331   */
332  public void replaceFacebookClient(FacebookClient facebookClient) {
333    this.facebookClient = facebookClient;
334  }
335
336  /**
337   * returns the JSON this connection is based on, it can be used for debug logs for example
338   *
339   * @return JSON as String the connection is based on
340   */
341  public String getJson() {
342    return json;
343  }
344
345  /**
346   * Returns the metadata of the response that created this {@link Connection}.
347   *
348   * @return response metadata or {@code null} if unavailable
349   */
350  public ResponseMetadata getResponseMetadata() {
351    return responseMetadata;
352  }
353
354  /**
355   * Attaches response metadata to this connection.
356   *
357   * @param responseMetadata
358   *          metadata captured while fetching the connection
359   */
360  public void setResponseMetadata(ResponseMetadata responseMetadata) {
361    this.responseMetadata = responseMetadata;
362  }
363
364  /**
365   * Override the next page URL - use at your own risk
366   *
367   * @param nextPageUrl
368   *          Custom next Page URL as String
369   */
370  public void setNextPageUrl(String nextPageUrl) {
371    this.nextPageUrl = nextPageUrl;
372  }
373
374  /**
375   * Override the previous page URL - use at your own risk
376   *
377   * @param previousPageUrl
378   *          Custom previous Page URL as String
379   */
380  public void setPreviousPageUrl(String previousPageUrl) {
381    this.previousPageUrl = previousPageUrl;
382  }
383
384  /**
385   * fetch the currently used {@see FacebookClient}
386   *
387   * @return the used FacebookClient
388   */
389  public FacebookClient getFacebookClient() {
390    return facebookClient;
391  }
392}