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 ≥ 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}