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