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