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