001package com.box.sdk; 002 003import java.util.ArrayList; 004import java.util.Collection; 005 006import com.eclipsesource.json.JsonArray; 007import com.eclipsesource.json.JsonObject; 008import com.eclipsesource.json.JsonValue; 009 010/** 011 * Receives real-time events from the API and forwards them to {@link EventListener EventListeners}. 012 * 013 * <p>This class handles long polling the Box events endpoint in order to receive real-time user events. 014 * When an EventStream is started, it begins long polling on a separate thread until the {@link #stop} method 015 * is called. 016 * Since the API may return duplicate events, EventStream also maintains a small cache of the most recently received 017 * event IDs in order to automatically deduplicate events.</p> 018 * <p>Note: Enterprise Events can be accessed by admin users with the EventLog.getEnterpriseEvents method</p> 019 * 020 */ 021public class EventStream { 022 private static final int LIMIT = 800; 023 private static final URLTemplate EVENT_URL = new URLTemplate("events?limit=" + LIMIT + "&stream_position=%s"); 024 private static final int STREAM_POSITION_NOW = -1; 025 026 private final BoxAPIConnection api; 027 private final long startingPosition; 028 private final Collection<EventListener> listeners; 029 private final Object listenerLock; 030 031 private LRUCache<String> receivedEvents; 032 private boolean started; 033 private Poller poller; 034 private Thread pollerThread; 035 036 /** 037 * Constructs an EventStream using an API connection. 038 * @param api the API connection to use. 039 */ 040 public EventStream(BoxAPIConnection api) { 041 this(api, STREAM_POSITION_NOW); 042 } 043 044 /** 045 * Constructs an EventStream using an API connection and a starting initial position. 046 * @param api the API connection to use. 047 * @param startingPosition the starting position of the event stream. 048 */ 049 public EventStream(BoxAPIConnection api, long startingPosition) { 050 this.api = api; 051 this.startingPosition = startingPosition; 052 this.listeners = new ArrayList<EventListener>(); 053 this.listenerLock = new Object(); 054 } 055 056 /** 057 * Adds a listener that will be notified when an event is received. 058 * @param listener the listener to add. 059 */ 060 public void addListener(EventListener listener) { 061 synchronized (this.listenerLock) { 062 this.listeners.add(listener); 063 } 064 } 065 066 /** 067 * Indicates whether or not this EventStream has been started. 068 * @return true if this EventStream has been started; otherwise false. 069 */ 070 public boolean isStarted() { 071 return this.started; 072 } 073 074 /** 075 * Stops this EventStream and disconnects from the API. 076 * @throws IllegalStateException if the EventStream is already stopped. 077 */ 078 public void stop() { 079 if (!this.started) { 080 throw new IllegalStateException("Cannot stop the EventStream because it isn't started."); 081 } 082 083 this.started = false; 084 this.pollerThread.interrupt(); 085 } 086 087 /** 088 * Starts this EventStream and begins long polling the API. 089 * @throws IllegalStateException if the EventStream is already started. 090 */ 091 public void start() { 092 if (this.started) { 093 throw new IllegalStateException("Cannot start the EventStream because it isn't stopped."); 094 } 095 096 final long initialPosition; 097 098 if (this.startingPosition == STREAM_POSITION_NOW) { 099 BoxAPIRequest request = new BoxAPIRequest(this.api, EVENT_URL.build(this.api.getBaseURL(), "now"), "GET"); 100 BoxJSONResponse response = (BoxJSONResponse) request.send(); 101 JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); 102 initialPosition = jsonObject.get("next_stream_position").asLong(); 103 } else { 104 initialPosition = this.startingPosition; 105 } 106 107 this.poller = new Poller(initialPosition); 108 109 this.pollerThread = new Thread(this.poller); 110 this.pollerThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { 111 public void uncaughtException(Thread t, Throwable e) { 112 EventStream.this.notifyException(e); 113 } 114 }); 115 this.pollerThread.start(); 116 117 this.started = true; 118 } 119 120 /** 121 * Indicates whether or not an event ID is a duplicate. 122 * 123 * <p>This method can be overridden by a subclass in order to provide custom de-duping logic.</p> 124 * 125 * @param eventID the event ID. 126 * @return true if the event is a duplicate; otherwise false. 127 */ 128 protected boolean isDuplicate(String eventID) { 129 if (this.receivedEvents == null) { 130 this.receivedEvents = new LRUCache<String>(); 131 } 132 133 return !this.receivedEvents.add(eventID); 134 } 135 136 private void notifyNextPosition(long position) { 137 synchronized (this.listenerLock) { 138 for (EventListener listener : this.listeners) { 139 listener.onNextPosition(position); 140 } 141 } 142 } 143 144 private void notifyEvent(BoxEvent event) { 145 synchronized (this.listenerLock) { 146 boolean isDuplicate = this.isDuplicate(event.getID()); 147 if (!isDuplicate) { 148 for (EventListener listener : this.listeners) { 149 listener.onEvent(event); 150 } 151 } 152 } 153 } 154 155 private void notifyException(Throwable e) { 156 if (e instanceof InterruptedException && !this.started) { 157 return; 158 } 159 160 this.stop(); 161 synchronized (this.listenerLock) { 162 for (EventListener listener : this.listeners) { 163 if (listener.onException(e)) { 164 return; 165 } 166 } 167 } 168 } 169 170 private class Poller implements Runnable { 171 private final long initialPosition; 172 173 private RealtimeServerConnection server; 174 175 public Poller(long initialPosition) { 176 this.initialPosition = initialPosition; 177 this.server = new RealtimeServerConnection(EventStream.this.api); 178 } 179 180 @Override 181 public void run() { 182 long position = this.initialPosition; 183 while (!Thread.interrupted()) { 184 if (this.server.getRemainingRetries() == 0) { 185 this.server = new RealtimeServerConnection(EventStream.this.api); 186 } 187 188 if (this.server.waitForChange(position)) { 189 if (Thread.interrupted()) { 190 return; 191 } 192 193 BoxAPIRequest request = new BoxAPIRequest(EventStream.this.api, 194 EVENT_URL.build(EventStream.this.api.getBaseURL(), position), "GET"); 195 BoxJSONResponse response = (BoxJSONResponse) request.send(); 196 JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); 197 JsonArray entriesArray = jsonObject.get("entries").asArray(); 198 for (JsonValue entry : entriesArray) { 199 BoxEvent event = new BoxEvent(EventStream.this.api, entry.asObject()); 200 EventStream.this.notifyEvent(event); 201 } 202 position = jsonObject.get("next_stream_position").asLong(); 203 EventStream.this.notifyNextPosition(position); 204 } 205 } 206 } 207 } 208}