001package com.box.sdk; 002 003import com.eclipsesource.json.Json; 004import com.eclipsesource.json.JsonArray; 005import com.eclipsesource.json.JsonObject; 006import com.eclipsesource.json.JsonValue; 007import java.net.MalformedURLException; 008import java.net.URL; 009import java.util.Iterator; 010import java.util.NoSuchElementException; 011 012/** 013 * Common implementation for paging support. 014 * 015 * @param <T> type of iterated entity 016 */ 017public abstract class BoxResourceIterable<T> implements Iterable<T> { 018 019 /** 020 * Parameter for max page size. 021 */ 022 public static final String PARAMETER_LIMIT = "limit"; 023 024 /** 025 * Parameter for marker for the beginning of next page. 026 */ 027 public static final String PARAMETER_MARKER = "marker"; 028 029 /** 030 * Body Parameter for marker for the beginning of next page. 031 */ 032 public static final String BODY_PARAMETER_MARKER_NEXT = "next_marker"; 033 034 /** 035 * Body parameter for page entries. 036 */ 037 public static final String BODY_PARAMETER_ENTRIES = "entries"; 038 039 /** 040 * The API connection to be used by the resource. 041 */ 042 private final BoxAPIConnection api; 043 044 /** 045 * To end-point with paging support. 046 */ 047 private final URL url; 048 049 /** 050 * The maximum number of items to return in a page. 051 */ 052 private final int limit; 053 054 /** 055 * The iterator that gets the next items. 056 */ 057 private final IteratorImpl iterator; 058 059 /** 060 * Constructor. 061 * 062 * @param api the API connection to be used by the resource 063 * @param url endpoint with paging support 064 * @param limit the maximum number of items to return in a page 065 */ 066 public BoxResourceIterable(BoxAPIConnection api, URL url, int limit) { 067 this(api, url, limit, null, null); 068 } 069 070 /** 071 * Constructor. 072 * 073 * @param api the API connection to be used by the resource. 074 * @param url to endpoint with paging support. 075 * @param limit the maximum number of items to return in a page. 076 * @param body the body to send to the requested endpoint. 077 */ 078 public BoxResourceIterable(BoxAPIConnection api, URL url, int limit, JsonObject body) { 079 this(api, url, limit, body, null); 080 } 081 082 /** 083 * Constructor. 084 * 085 * @param api the API connection to be used by the resource. 086 * @param url to endpoint with paging support. 087 * @param limit the maximum number of items to return in a page. 088 * @param marker the marker where the iterator will begin 089 */ 090 public BoxResourceIterable(BoxAPIConnection api, URL url, int limit, String marker) { 091 this(api, url, limit, null, marker); 092 } 093 094 /** 095 * Constructor. 096 * 097 * @param api the API connection to be used by the resource. 098 * @param url to endpoint with paging support. 099 * @param limit the maximum number of items to return in a page. 100 * @param body the body to send to the requested endpoint. 101 * @param marker the marker where the iterator will begin 102 */ 103 public BoxResourceIterable(BoxAPIConnection api, URL url, int limit, JsonObject body, String marker) { 104 this.api = api; 105 this.url = url; 106 this.limit = limit; 107 this.iterator = new IteratorImpl(marker, body); 108 } 109 110 /** 111 * Factory to build a new instance for a received JSON item. 112 * 113 * @param jsonObject of the item 114 * @return the item instance 115 */ 116 protected abstract T factory(JsonObject jsonObject); 117 118 /** 119 * Builds internal read-only iterator over {@link BoxResource}-s. 120 * 121 * @return iterator implementation 122 * @see Iterable#iterator() 123 */ 124 @Override 125 public Iterator<T> iterator() { 126 return this.iterator; 127 } 128 129 /** 130 * Builds internal read-only iterator over {@link BoxResource}-s. 131 * 132 * @return iterator implementation 133 * @see Iterable#iterator() 134 */ 135 public String getNextMarker() { 136 return this.iterator.markerNext; 137 } 138 139 /** 140 * Paging implementation. 141 */ 142 private class IteratorImpl implements Iterator<T> { 143 144 /** 145 * Base 64 encoded string that represents where the paging should being. It should be left blank to begin 146 * paging. 147 */ 148 private String markerNext; 149 150 /** 151 * Current loaded page. 152 */ 153 private JsonArray page; 154 155 /** 156 * Cursor within the page (index of a next item for read). 157 */ 158 private int pageCursor; 159 160 /** 161 * The body to include in the request. 162 */ 163 private JsonObject body; 164 165 /** 166 * Constructor. 167 * 168 * @param marker the marker at which the iterator will begin 169 * @param body Request body 170 */ 171 IteratorImpl(String marker, JsonObject body) { 172 this.markerNext = marker; 173 this.body = body; 174 this.loadNextPage(); 175 } 176 177 /** 178 * Loads next page. 179 */ 180 private void loadNextPage() { 181 String existingQuery = BoxResourceIterable.this.url.getQuery(); 182 QueryStringBuilder builder = new QueryStringBuilder(existingQuery); 183 builder.appendParam(PARAMETER_LIMIT, BoxResourceIterable.this.limit); 184 if (this.markerNext != null) { 185 if (this.body != null) { 186 this.body.set("marker", this.markerNext); 187 } else { 188 builder.appendParam(PARAMETER_MARKER, this.markerNext); 189 } 190 } 191 192 URL url; 193 try { 194 url = builder.replaceQuery(BoxResourceIterable.this.url); 195 } catch (MalformedURLException e) { 196 throw new BoxAPIException("Couldn't append a query string to the provided URL."); 197 } 198 199 BoxJSONRequest request; 200 if (this.body != null) { 201 request = new BoxJSONRequest(BoxResourceIterable.this.api, url, "POST"); 202 request.setBody(this.body.toString()); 203 } else { 204 request = new BoxJSONRequest(BoxResourceIterable.this.api, url, "GET"); 205 } 206 207 try (BoxJSONResponse response = request.send()) { 208 JsonObject pageBody = Json.parse(response.getJSON()).asObject(); 209 210 JsonValue markerNextValue = pageBody.get(BODY_PARAMETER_MARKER_NEXT); 211 if (markerNextValue != null && markerNextValue.isString()) { 212 this.markerNext = markerNextValue.asString(); 213 } else { 214 this.markerNext = null; 215 } 216 217 this.page = pageBody.get(BODY_PARAMETER_ENTRIES).asArray(); 218 this.pageCursor = 0; 219 } 220 } 221 222 /** 223 * {@inheritDoc} 224 */ 225 @Override 226 public boolean hasNext() { 227 if (this.pageCursor < this.page.size()) { 228 return true; 229 } 230 if (this.markerNext == null || this.markerNext.isEmpty()) { 231 return false; 232 } 233 this.loadNextPage(); 234 return !this.page.isEmpty(); 235 } 236 237 /** 238 * {@inheritDoc} 239 */ 240 @Override 241 public T next() { 242 if (!this.hasNext()) { 243 throw new NoSuchElementException(); 244 } 245 246 JsonObject entry = this.page.get(this.pageCursor++).asObject(); 247 return BoxResourceIterable.this.factory(entry); 248 } 249 250 /** 251 * @throws UnsupportedOperationException Remove is not supported 252 */ 253 @Override 254 public void remove() { 255 throw new UnsupportedOperationException(); 256 } 257 } 258 259}