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