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