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