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}