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