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