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