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.Collection;
010import java.util.Date;
011import java.util.Iterator;
012import java.util.LinkedHashSet;
013import java.util.Set;
014
015/**
016 * A log of events that were retrieved from the events endpoint.
017 *
018 * <p>An EventLog cannot be instantiated directly. Instead, use one of the static methods to
019 * retrieve a log of events. Unlike the {@link EventStream} class, EventLog doesn't support
020 * retrieving events in real-time.
021 */
022public class EventLog implements Iterable<BoxEvent> {
023
024  static final int ENTERPRISE_LIMIT = 500;
025  /** Enterprise Event URL Template. */
026  public static final URLTemplate ENTERPRISE_EVENT_URL_TEMPLATE =
027      new URLTemplate("events?stream_type=admin_logs&" + "limit=" + ENTERPRISE_LIMIT);
028
029  private final int chunkSize;
030  private final int limit;
031  private final String nextStreamPosition;
032  private final String streamPosition;
033  private final Set<BoxEvent> events;
034
035  private Date startDate;
036  private Date endDate;
037
038  EventLog(BoxAPIConnection api, JsonObject json, String streamPosition, int limit) {
039    this.streamPosition = streamPosition;
040    this.limit = limit;
041    JsonValue nextStreamPosition = json.get("next_stream_position");
042    if (nextStreamPosition.isString()) {
043      this.nextStreamPosition = nextStreamPosition.asString();
044    } else {
045      this.nextStreamPosition = nextStreamPosition.toString();
046    }
047    this.chunkSize = json.get("chunk_size").asInt();
048
049    this.events = new LinkedHashSet<>(this.chunkSize);
050    JsonArray entries = json.get("entries").asArray();
051    for (JsonValue entry : entries) {
052      this.events.add(new BoxEvent(api, entry.asObject()));
053    }
054  }
055
056  /**
057   * Method reads from the `admin-logs` stream and returns {@link BoxEvent} {@link Iterator}. The
058   * emphasis for this stream is on completeness over latency, which means that Box will deliver
059   * admin events in chronological order and without duplicates, but with higher latency. You can
060   * specify start and end time/dates. This method will only work with an API connection for an
061   * enterprise admin account or service account with manage enterprise properties. You can specify
062   * a date range to limit when events occured, starting from a given position within the event
063   * stream, set limit or specify event types that should be filtered. Example:
064   *
065   * <pre>{@code
066   * EnterpriseEventsRequest request = new EnterpriseEventsRequest()
067   *     .after(after)        // The lower bound on the timestamp of the events returned.
068   *     .before(before)      // The upper bound on the timestamp of the events returned.
069   *     .limit(20)           // The number of entries to be returned in the response.
070   *     .position(position)  // The starting position of the event stream.
071   *     .types(EventType.LOGIN, EventType.FAILED_LOGIN); // List of event types to filter by.
072   * EventLog.getEnterpriseEvents(api, request);
073   * }</pre>
074   *
075   * @param api the API connection to use.
076   * @param enterpriseEventsRequest request to get events.
077   * @return a log of all the events that met the given criteria.
078   */
079  public static EventLog getEnterpriseEvents(
080      BoxAPIConnection api, EnterpriseEventsRequest enterpriseEventsRequest) {
081    EventLogRequest request =
082        new EventLogRequest(
083            enterpriseEventsRequest.getBefore(),
084            enterpriseEventsRequest.getAfter(),
085            enterpriseEventsRequest.getPosition(),
086            enterpriseEventsRequest.getLimit(),
087            enterpriseEventsRequest.getTypes());
088    return getEnterpriseEventsForStreamType(api, enterpriseEventsRequest.getStreamType(), request);
089  }
090
091  /**
092   * Method reads from the `admin-logs-streaming` stream and returns {@link BoxEvent} {@link
093   * Iterator} The emphasis for this feed is on low latency rather than chronological accuracy,
094   * which means that Box may return events more than once and out of chronological order. Events
095   * are returned via the API around 12 seconds after they are processed by Box (the 12 seconds
096   * buffer ensures that new events are not written after your cursor position). Only two weeks of
097   * events are available via this feed, and you cannot set start and end time/dates. This method
098   * will only work with an API connection for an enterprise admin account or service account with
099   * manage enterprise properties. You can specify a starting from a given position within the event
100   * stream, set limit or specify event types that should be filtered. Example:
101   *
102   * <pre>{@code
103   * EnterpriseEventsStreamRequest request = new EnterpriseEventsStreamRequest()
104   *     .limit(200)          // The number of entries to be returned in the response.
105   *     .position(position)  // The starting position of the event stream.
106   *     .types(EventType.LOGIN, EventType.FAILED_LOGIN); // List of event types to filter by.
107   * EventLog.getEnterpriseEventsStream(api, request);
108   * }</pre>
109   *
110   * @param api the API connection to use.
111   * @param enterpriseEventsStreamRequest request to get events.
112   * @return a log of all the events that met the given criteria.
113   */
114  public static EventLog getEnterpriseEventsStream(
115      BoxAPIConnection api, EnterpriseEventsStreamRequest enterpriseEventsStreamRequest) {
116    EventLogRequest request =
117        new EventLogRequest(
118            null,
119            null,
120            enterpriseEventsStreamRequest.getPosition(),
121            enterpriseEventsStreamRequest.getLimit(),
122            enterpriseEventsStreamRequest.getTypes());
123    return getEnterpriseEventsForStreamType(
124        api, enterpriseEventsStreamRequest.getStreamType(), request);
125  }
126
127  private static EventLog getEnterpriseEventsForStreamType(
128      BoxAPIConnection api, String streamType, EventLogRequest request) {
129    URL url = new URLTemplate("events?").build(api.getBaseURL());
130    QueryStringBuilder queryBuilder = new QueryStringBuilder(url.getQuery());
131    queryBuilder.appendParam("stream_type", streamType);
132    addParamsToQuery(request, queryBuilder);
133
134    try {
135      url = queryBuilder.addToURL(url);
136    } catch (MalformedURLException e) {
137      throw new BoxAPIException("Couldn't append a query string to the provided URL.");
138    }
139
140    BoxJSONRequest apiRequest = new BoxJSONRequest(api, url, "GET");
141    try (BoxJSONResponse response = apiRequest.send()) {
142      JsonObject responseJSON = Json.parse(response.getJSON()).asObject();
143      EventLog log = new EventLog(api, responseJSON, request.getPosition(), request.getLimit());
144      log.setStartDate(request.getAfter());
145      log.setEndDate(request.getBefore());
146      return log;
147    }
148  }
149
150  private static void addParamsToQuery(EventLogRequest request, QueryStringBuilder queryBuilder) {
151    queryBuilder.appendParam("limit", request.getLimit());
152
153    if (request.getAfter() != null) {
154      queryBuilder.appendParam("created_after", BoxDateFormat.format(request.getAfter()));
155    }
156    if (request.getBefore() != null) {
157      queryBuilder.appendParam("created_before", BoxDateFormat.format(request.getBefore()));
158    }
159    if (request.getPosition() != null) {
160      queryBuilder.appendParam("stream_position", request.getPosition());
161    }
162    if (request.getTypes().size() > 0) {
163      String types = String.join(",", request.getTypes());
164      queryBuilder.appendParam("event_type", types);
165    }
166  }
167
168  /**
169   * Returns an iterator over the events in this log.
170   *
171   * @return an iterator over the events in this log.
172   */
173  @Override
174  public Iterator<BoxEvent> iterator() {
175    return this.events.iterator();
176  }
177
178  /**
179   * Gets the date of the earliest event in this log.
180   *
181   * <p>The value returned by this method corresponds to the <code>created_after</code> URL
182   * parameter that was used when retrieving the events in this EventLog.
183   *
184   * @return the date of the earliest event in this log.
185   */
186  public Date getStartDate() {
187    return this.startDate;
188  }
189
190  void setStartDate(Date startDate) {
191    this.startDate = startDate;
192  }
193
194  /**
195   * Gets the date of the latest event in this log.
196   *
197   * <p>The value returned by this method corresponds to the <code>created_before</code> URL
198   * parameter that was used when retrieving the events in this EventLog.
199   *
200   * @return the date of the latest event in this log.
201   */
202  public Date getEndDate() {
203    return this.endDate;
204  }
205
206  void setEndDate(Date endDate) {
207    this.endDate = endDate;
208  }
209
210  /**
211   * Gets the maximum number of events that this event log could contain given its start date, end
212   * date, and stream position.
213   *
214   * <p>The value returned by this method corresponds to the <code>limit</code> URL parameter that
215   * was used when retrieving the events in this EventLog.
216   *
217   * @return the maximum number of events.
218   */
219  public int getLimit() {
220    return this.limit;
221  }
222
223  /**
224   * Gets the starting position of the events in this log within the event stream.
225   *
226   * <p>The value returned by this method corresponds to the <code>stream_position</code> URL
227   * parameter that was used when retrieving the events in this EventLog.
228   *
229   * @return the starting position within the event stream.
230   */
231  public String getStreamPosition() {
232    return this.streamPosition;
233  }
234
235  /**
236   * Gets the next position within the event stream for retrieving subsequent events.
237   *
238   * <p>The value returned by this method corresponds to the <code>next_stream_position</code> field
239   * returned by the API's events endpoint.
240   *
241   * @return the next position within the event stream.
242   */
243  public String getNextStreamPosition() {
244    return this.nextStreamPosition;
245  }
246
247  /**
248   * Gets the number of events in this log, including duplicate events.
249   *
250   * <p>The chunk size may not be representative of the number of events returned by this EventLog's
251   * iterator because the iterator will automatically ignore duplicate events.
252   *
253   * <p>The value returned by this method corresponds to the <code>chunk_size</code> field returned
254   * by the API's events endpoint.
255   *
256   * @return the number of events, including duplicates.
257   */
258  public int getChunkSize() {
259    return this.chunkSize;
260  }
261
262  /**
263   * Gets the number of events in this list, excluding duplicate events.
264   *
265   * <p>The size is guaranteed to be representative of the number of events returned by this
266   * EventLog's iterator.
267   *
268   * @return the number of events, excluding duplicates.
269   */
270  public int getSize() {
271    return this.events.size();
272  }
273
274  private static final class EventLogRequest {
275    private final Date before;
276    private final Date after;
277    private final String position;
278    private final Integer limit;
279    private final Collection<String> types;
280
281    private EventLogRequest(
282        Date before, Date after, String position, Integer limit, Collection<String> types) {
283      this.before = before;
284      this.after = after;
285      this.position = position;
286      this.limit = limit;
287      this.types = types;
288    }
289
290    private Date getBefore() {
291      return before;
292    }
293
294    private Date getAfter() {
295      return after;
296    }
297
298    private String getPosition() {
299      return position;
300    }
301
302    private Integer getLimit() {
303      return limit;
304    }
305
306    private Collection<String> getTypes() {
307      return types;
308    }
309  }
310}