diff --git a/nbproject/project.properties b/nbproject/project.properties index 41de61c..1a2e003 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -83,5 +83,6 @@ run.test.classpath=\ ${javac.test.classpath}:\ ${build.test.classes.dir} source.encoding=UTF-8 +source.reference.resty-0.3.2.jar=/Users/jpasqua/Dropbox/Dev/ThirdParty/resty/src/main/java/ src.dir=src test.src.dir=test diff --git a/src/org/noroomattheinn/tesla/APICall.java b/src/org/noroomattheinn/tesla/APICall.java index a762745..495d006 100644 --- a/src/org/noroomattheinn/tesla/APICall.java +++ b/src/org/noroomattheinn/tesla/APICall.java @@ -7,11 +7,10 @@ package org.noroomattheinn.tesla; import java.io.IOException; -import java.util.Date; import java.util.logging.Level; +import org.noroomattheinn.utils.RestyWrapper; import us.monoid.json.JSONException; import us.monoid.json.JSONObject; -import us.monoid.web.Resty; /** * APICall: This class is the parent of all API interactions for State and @@ -28,12 +27,9 @@ */ public abstract class APICall { - private static double MaxRequestRate = 20.0 / (1000.0 * 60.0); // 20 requests/minute - private static long startTime = new Date().getTime(); - protected static long requestCount = 0; // Instance Variables - private Resty api; + private RestyWrapper api; private String vid; private JSONObject theState; private String endpoint; @@ -70,9 +66,7 @@ public final boolean setAndRefresh(String newEndpoint) { public boolean refresh() { try { - honorRateLimit(); if (endpoint != null) { - requestCount++; // Count it even if it fails setState(api.json(endpoint).object()); } return true; @@ -125,24 +119,4 @@ public String toString() { } } - protected void honorRateLimit() { - while (true) { - if (requestCount < 30) return; // Don't worry too much until there is some history - - long now = new Date().getTime(); - long elapsedMillis = now - startTime; - double rate = ((double) requestCount) / elapsedMillis; - if (rate > MaxRequestRate) { - try { - Tesla.logger.log( - Level.INFO, "Throttling request rate. Requests: {0}, Millis: {1}\n", - new Object[]{requestCount, elapsedMillis}); - Thread.sleep(5 * 1000); // Arbitrary amount of wait time - } catch (InterruptedException ex) { - Tesla.logger.log(Level.SEVERE, null, ex); - } - } else return; - } - } - } diff --git a/src/org/noroomattheinn/tesla/GUIState.java b/src/org/noroomattheinn/tesla/GUIState.java index b1d1328..c573937 100644 --- a/src/org/noroomattheinn/tesla/GUIState.java +++ b/src/org/noroomattheinn/tesla/GUIState.java @@ -23,6 +23,10 @@ public GUIState(Vehicle v) { super(v, Tesla.command(v.getVID(), "gui_settings")); } + public boolean refresh() { + return super.refresh(); + } + // // Field Accessor Methods // diff --git a/src/org/noroomattheinn/tesla/Options.java b/src/org/noroomattheinn/tesla/Options.java index 60268ea..c1a1a0e 100644 --- a/src/org/noroomattheinn/tesla/Options.java +++ b/src/org/noroomattheinn/tesla/Options.java @@ -236,6 +236,7 @@ public enum SeatType { IPMB("Leather, Black"), IPMG("Leather, Gray"), IPMT("Leather, Tan"), + IZZW("Perf Leather with Grey Piping, White"), IZMB("Perf Leather with Piping, Black"), IZMG("Perf Leather with Piping, Gray"), IZMT("Perf Leather with Piping, Tan"), diff --git a/src/org/noroomattheinn/tesla/SnapshotState.java b/src/org/noroomattheinn/tesla/SnapshotState.java index 5c716b2..00d1352 100644 --- a/src/org/noroomattheinn/tesla/SnapshotState.java +++ b/src/org/noroomattheinn/tesla/SnapshotState.java @@ -9,16 +9,16 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.net.URLConnection; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.logging.Level; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; +import org.noroomattheinn.utils.RestyWrapper; +import org.noroomattheinn.utils.Utils; import us.monoid.json.JSONException; import us.monoid.json.JSONObject; -import us.monoid.web.Resty; import us.monoid.web.TextResource; /** @@ -107,6 +107,7 @@ public boolean refreshStream() { return true; } reader = null; + Utils.sleep(1000); } invalidate(); return false; @@ -168,6 +169,7 @@ private JSONObject produce(BufferedReader reader) { private void prepare() { prepareInternal(); if (reader != null) return; + Utils.sleep(1000); prepareInternal(); // Try again. Auth tokens may have expired. } @@ -175,7 +177,6 @@ private void prepare() { private void prepareInternal() { if (reader != null) return; - if (vehicleWithToken == null) { vehicleWithToken = getVehicleWithAuthToken(v); if (vehicleWithToken == null) return; @@ -184,9 +185,6 @@ private void prepareInternal() { String endpoint = String.format( endpointFormat, vehicleWithToken.getStreamingVID(), allKeys); - honorRateLimit(); - requestCount++; // Count it even if it fails... - try { TextResource r = getAuthAPI(vehicleWithToken).text(endpoint); reader = new BufferedReader(new InputStreamReader(r.stream())); @@ -205,56 +203,50 @@ private void prepareInternal() { * *----------------------------------------------------------------------------*/ - private void setAuthHeader(Resty api, String username, String authToken) { - byte[] authString = (username + ":" + authToken).getBytes(); - String encodedString = Base64.encodeBase64String(authString); - api.withHeader("Authorization", "Basic " + encodedString); - } + private void setAuthHeader(RestyWrapper api, String username, String authToken) { + byte[] authString = (username + ":" + authToken).getBytes(); + String encodedString = Base64.encodeBase64String(authString); + api.withHeader("Authorization", "Basic " + encodedString); + } - private Vehicle getVehicleWithAuthToken(Vehicle basedOn) { - String vid = basedOn.getVID(); - // Remember this so we can find the right vehicle when we fetch - // the updated list of vehicles after doing the wakeup + private Vehicle getVehicleWithAuthToken(Vehicle basedOn) { + String vid = basedOn.getVID(); + // Remember this so we can find the right vehicle when we fetch + // the updated list of vehicles after doing the wakeup - ActionController a = new ActionController(basedOn); - for (int i = 0; i < WakeupRetries; i++) { - a.wakeUp(); + ActionController a = new ActionController(basedOn); + for (int i = 0; i < WakeupRetries; i++) { - List vList = new ArrayList<>(); - basedOn.getContext().fetchVehiclesInto(vList); - for (Vehicle newV : vList) { - if (newV.getVID().equals(vid) && newV.getStreamingToken() != null) { - return newV; - } + List vList = new ArrayList<>(); + basedOn.getContext().fetchVehiclesInto(vList); + for (Vehicle newV : vList) { + if (newV.getVID().equals(vid) && newV.getStreamingToken() != null) { + return newV; } } - - // For some reason we can't get Streaming tokens. We've tried enough - // so give up - Tesla.logger.log(Level.WARNING, "Error: couldn't retreive auth tokens"); - return null; + a.wakeUp(); Utils.sleep(500); } - private Resty getAuthAPI(Vehicle v) { - String authToken = v.getStreamingToken(); - - // This call requires BASIC authentication using the user name (this is - // the user's registered email address) and the authToken. - // We can't use the Resty authentication mechanism because the tesla - // site doesn't seem to request authentication - it just expects the - // Authorization header feld to be present. - // To accomplish that, create a new (temporary) Resty instance and - // add the auth header to it. - Resty api = new Resty(new ReadTimeoutOption()); - setAuthHeader(api, v.getContext().getUsername(), authToken); - return api; - } + // For some reason we can't get Streaming tokens. We've tried enough + // so give up + Tesla.logger.log(Level.WARNING, "Error: couldn't retreive auth tokens"); + return null; + } - private class ReadTimeoutOption extends Resty.Option { - public void apply(URLConnection aConnection) { - aConnection.setReadTimeout(ReadTimeoutInMillis); - } - } + private RestyWrapper getAuthAPI(Vehicle v) { + String authToken = v.getStreamingToken(); + + // This call requires BASIC authentication using the user name (this is + // the user's registered email address) and the authToken. + // We can't use the Resty authentication mechanism because the tesla + // site doesn't seem to request authentication - it just expects the + // Authorization header feld to be present. + // To accomplish that, create a new (temporary) Resty instance and + // add the auth header to it. + RestyWrapper api = new RestyWrapper(ReadTimeoutInMillis); + setAuthHeader(api, v.getContext().getUsername(), authToken); + return api; + } - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/org/noroomattheinn/tesla/StreamingState.java b/src/org/noroomattheinn/tesla/StreamingState.java index a71b5c1..3824552 100644 --- a/src/org/noroomattheinn/tesla/StreamingState.java +++ b/src/org/noroomattheinn/tesla/StreamingState.java @@ -9,7 +9,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.net.URLConnection; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -19,9 +18,9 @@ import java.util.logging.Level; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; +import org.noroomattheinn.utils.RestyWrapper; import us.monoid.json.JSONException; import us.monoid.json.JSONObject; -import us.monoid.web.Resty; import us.monoid.web.TextResource; /** @@ -232,9 +231,7 @@ private BufferedReader prepareToProduce() { String endpoint = String.format( endpointFormat, withToken.getStreamingVID(), allKeys); - honorRateLimit(); TextResource r = getAuthAPI(withToken).text(endpoint); - requestCount++; reader = new BufferedReader(new InputStreamReader(r.stream())); } catch (IOException ex) { // Timed out or other problem @@ -243,7 +240,7 @@ private BufferedReader prepareToProduce() { return reader; } - private void setAuthHeader(Resty api, String username, String authToken) { + private void setAuthHeader(RestyWrapper api, String username, String authToken) { byte[] authString = (username + ":" + authToken).getBytes(); String encodedString = Base64.encodeBase64String(authString); api.withHeader("Authorization", "Basic " + encodedString); @@ -274,7 +271,7 @@ private Vehicle getVehicleWithAuthToken(Vehicle basedOn) { return null; } - private Resty getAuthAPI(Vehicle v) { + private RestyWrapper getAuthAPI(Vehicle v) { String authToken = v.getStreamingToken(); // This call requires BASIC authentication using the user name (this is @@ -284,16 +281,11 @@ private Resty getAuthAPI(Vehicle v) { // Authorization header feld to be present. // To accomplish that, create a new (temporary) Resty instance and // add the auth header to it. - Resty api = new Resty(new ReadTimeoutOption()); + RestyWrapper api = new RestyWrapper(ReadTimeoutInMillis); setAuthHeader(api, v.getContext().getUsername(), authToken); return api; } - private class ReadTimeoutOption extends Resty.Option { - public void apply(URLConnection aConnection) { - aConnection.setReadTimeout(ReadTimeoutInMillis); - } - } } diff --git a/src/org/noroomattheinn/tesla/Tesla.java b/src/org/noroomattheinn/tesla/Tesla.java index 0e7b63d..2198f13 100644 --- a/src/org/noroomattheinn/tesla/Tesla.java +++ b/src/org/noroomattheinn/tesla/Tesla.java @@ -6,6 +6,7 @@ package org.noroomattheinn.tesla; +import java.io.File; import java.io.IOException; import java.net.HttpCookie; import java.net.URI; @@ -15,10 +16,10 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.noroomattheinn.utils.CookieUtils; +import org.noroomattheinn.utils.RestyWrapper; import us.monoid.json.JSONArray; import us.monoid.json.JSONException; import us.monoid.web.FormContent; -import us.monoid.web.Resty; import us.monoid.web.TextResource; /** @@ -63,7 +64,7 @@ public class Tesla { } // Instance Variables - private Resty api; + private RestyWrapper api; private List vehicles; private String username = null; @@ -73,7 +74,7 @@ public class Tesla { // public Tesla() { - api = new Resty(); + api = new RestyWrapper(); vehicles = new ArrayList<>(); } @@ -124,7 +125,7 @@ private boolean login() { return false; } - private boolean login(String username, String password, boolean remember) { + private boolean login(String username, String password) { try { TextResource text; @@ -132,19 +133,16 @@ private boolean login(String username, String password, boolean remember) { // Do the first phase of the login. This sets the _s_portal_session // cookie. This must be followed by phase 2 of the login text = api.text(endpoint("login")); + if (!( text.status(200) || text.status(302) )) + return false; // OK, now complete the login by doing a POST with the username // and password. This sets the user_credentials cookie - FormContent fc = Resty.form( - "user_session[email]=" + Resty.enc(username) + - "&user_session[password]=" + Resty.enc(password)); + FormContent fc = RestyWrapper.form( + "user_session[email]=" + RestyWrapper.enc(username) + + "&user_session[password]=" + RestyWrapper.enc(password)); text = api.text(endpoint("login"), fc); - - // Save the login information for next time... - this.username = username; - stashUsername(); - if (remember) CookieUtils.fetchAndWriteCookies(CookiesFile); - return true; + return (text.status(200) || text.status(302)); } catch (IOException ex) { logger.log(Level.SEVERE, null, ex); return false; @@ -160,7 +158,14 @@ private boolean login(String username, String password, boolean remember) { */ public boolean connect() { vehicles.clear(); - return login() && fetchVehiclesInto(vehicles); + if (!login()) return false; + if (!fetchVehiclesInto(vehicles)) { + // Delete the cookies file if it exists - it's not working + File cf = new File(CookiesFile); + if (cf.exists()) cf.delete(); + return false; + } + return true; } /* @@ -172,7 +177,14 @@ public boolean connect() { */ public boolean connect(String username, String password, boolean remember) { vehicles.clear(); - return login(username, password, remember) && fetchVehiclesInto(vehicles); + if (!login(username, password)) return false; + if (!fetchVehiclesInto(vehicles)) return false; + + // OK, we made it! Stash off the results of the successful login + this.username = username; + stashUsername(); + if (remember) CookieUtils.fetchAndWriteCookies(CookiesFile); + return true; } @@ -189,7 +201,7 @@ boolean fetchVehiclesInto(List list) { list.add(vehicle); } } catch (IOException | JSONException ex) { - logger.log(Level.SEVERE, null, ex); + logger.log(Level.INFO, null, ex); return false; } return true; @@ -204,6 +216,6 @@ boolean fetchVehiclesInto(List list) { // public String getUsername() { return username; } - public Resty getAPI() { return api; } + public RestyWrapper getAPI() { return api; } } diff --git a/src/org/noroomattheinn/tesla/Vehicle.java b/src/org/noroomattheinn/tesla/Vehicle.java index 56ce3e7..539e07f 100644 --- a/src/org/noroomattheinn/tesla/Vehicle.java +++ b/src/org/noroomattheinn/tesla/Vehicle.java @@ -8,11 +8,11 @@ import java.io.IOException; import java.util.logging.Level; +import org.noroomattheinn.utils.RestyWrapper; import us.monoid.json.JSONArray; import us.monoid.json.JSONException; import us.monoid.json.JSONObject; import us.monoid.web.JSONResource; -import us.monoid.web.Resty; /** * Vehicle: This object represents a single Tesla Vehicle. All access to @@ -30,7 +30,7 @@ public class Vehicle { // Instance variables for the context in which this object was created private Tesla tesla; - private Resty api; + private RestyWrapper api; // Instance variables that describe a Vehicle private String UNKNOWN_color; @@ -42,8 +42,6 @@ public class Vehicle { private String streamingTokens[]; private String status; private Options options; - private GUIState cachedGUIState; - private VehicleState cachedVehicleState; // // Constructors @@ -74,13 +72,6 @@ public Vehicle(Tesla tesla, JSONObject description) { // Handle the Options options = new Options(description.optString("option_codes")); - - // Some options don't tend to change much, so get a copy here for reference - cachedGUIState = new GUIState(this); - while (cachedGUIState.refresh() == false) {} - - cachedVehicleState = new VehicleState(this); - while (cachedVehicleState.refresh() == false) {} } @@ -94,8 +85,6 @@ public Vehicle(Tesla tesla, JSONObject description) { public String status() { return status; } public Options getOptions() { return options; } public String getStreamingToken() { return streamingTokens[0]; } - public GUIState cachedGUIState() { return cachedGUIState; } - public VehicleState cachedVehicleState() { return cachedVehicleState; } public boolean mobileEnabled() { try { JSONResource resource = api.json(Tesla.endpoint(vehicleID, "mobile_enabled")); @@ -110,7 +99,7 @@ public boolean mobileEnabled() { // Methods to get context // - public Resty getAPI() { return api; } + public RestyWrapper getAPI() { return api; } public Tesla getContext() { return tesla; } diff --git a/src/org/noroomattheinn/utils/CircularBuffer.java b/src/org/noroomattheinn/utils/CircularBuffer.java new file mode 100644 index 0000000..573b8b6 --- /dev/null +++ b/src/org/noroomattheinn/utils/CircularBuffer.java @@ -0,0 +1,141 @@ +/* + * CircularBuffer.java - Copyright(c) 2013 Joe Pasqua + * Provided under the MIT License. See the LICENSE file for details. + * Created: Sep 28, 2013 + * + * NOTE: This code is based on the example shown here: + * http://bradforj287.blogspot.com/2010/11/efficient-circular-buffer-in-java.html + * Rights for that code are "feel free to use it and do whatever you'd like with it." + * + */ + +package org.noroomattheinn.utils; + +import java.util.NoSuchElementException; + +/** + * CircularBuffer: Thread-safe circular buffer backed by an array. It is + * possible to peek at the any element in the buffer, not just the first + * and last. + * + * @author Joe Pasqua + */ + +/** + * CircularBuffer backed by an array. The operations are thread-safe. + * + */ +public class CircularBuffer { + +/*------------------------------------------------------------------------------ + * + * Internal State + * + *----------------------------------------------------------------------------*/ + + private T[] data; // The data elements + private int front = 0; // The oldest element + private int insertLocation = 0; // Where to put the next piece of data + private int size = 0; // The number of elements in queue, not the capacity + + +/*============================================================================== + * ------- ------- + * ------- Public Interface To This Class ------- + * ------- ------- + *============================================================================*/ + + /** + * Creates a circular buffer with the specified size. + * + * @param bufferSize - the maximum size of the buffer + */ + public CircularBuffer(int bufferSize) { + data = Utils.cast(new Object[bufferSize]); + } + + /** + * Inserts an item at the end of the queue. If the queue is full, the oldest + * value will be removed and head of the queue will become the second oldest + * value. + * + * @param item - the item to be inserted + */ + public synchronized void insert(T item) { + data[insertLocation] = item; + insertLocation = (insertLocation + 1) % data.length; + + // If the queue is full, this means we just overwrote the front of the + // queue. So increment the front location. + if (size == data.length) { + front = (front + 1) % data.length; + } else { + size++; + } + } + + /** + * Returns the number of elements in the buffer (not the capacity) + * + * @return int - The number of elements inside this buffer + */ + public synchronized int size() { return size; } + + /** + * Returns the head element of the queue. + * + * @return The head element of the queue + */ + public synchronized T removeFront() { + if (size == 0) { + throw new NoSuchElementException(); + } + T retValue = data[front]; + front = (front + 1) % data.length; + size--; + return retValue; + } + + /** + * Returns the head of the queue but does not remove it. + * + * @return The head element of the queue + */ + public synchronized T peekFront() { + if (size == 0) { + return null; + } else { + return data[front]; + } + } + + /** + * Returns the nth element of the queue but does not remove it. + * + * @return T - The nth element + */ + public synchronized T peekAt(int n) { + if (size <= n) { + return null; + } else { + return data[(front + n) % data.length]; + } + } + + /** + * Returns the last element of the queue but does not remove it. + * + * @return T - The most recently added value + */ + public synchronized T peekLast() { + if (size == 0) { + return null; + } else { + int lastElement = insertLocation - 1; + if (lastElement < 0) { + lastElement = data.length - 1; + } + return data[lastElement]; + } + } +} \ No newline at end of file diff --git a/src/org/noroomattheinn/utils/Pair.java b/src/org/noroomattheinn/utils/Pair.java new file mode 100644 index 0000000..787425a --- /dev/null +++ b/src/org/noroomattheinn/utils/Pair.java @@ -0,0 +1,23 @@ +/* + * Pair.java - Copyright(c) 2013 Joe Pasqua + * Provided under the MIT License. See the LICENSE file for details. + * Created: Sep 28, 2013 + */ + +package org.noroomattheinn.utils; + +/** + * Pair: a 2-tuple + * + * @author Joe Pasqua + */ +public class Pair { + + public final T1 item1; + public final T2 item2; + + public Pair(T1 k, T2 v) { + this.item1 = k; + this.item2 = v; + } +} diff --git a/src/org/noroomattheinn/utils/RestyWrapper.java b/src/org/noroomattheinn/utils/RestyWrapper.java new file mode 100644 index 0000000..e68f444 --- /dev/null +++ b/src/org/noroomattheinn/utils/RestyWrapper.java @@ -0,0 +1,174 @@ +/* + * RestyWrapper.java - Copyright(c) 2013 Joe Pasqua + * Provided under the MIT License. See the LICENSE file for details. + * Created: Sep 28, 2013 + */ + +package org.noroomattheinn.utils; + +import java.io.IOException; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import us.monoid.web.AbstractContent; +import us.monoid.web.FormContent; +import us.monoid.web.JSONResource; +import us.monoid.web.Resty; +import us.monoid.web.TextResource; + +/** + * RestyWrapper: Wraps the Resty interface and throttles the request rate + * + * @author Joe Pasqua + */ +public class RestyWrapper { + +/*------------------------------------------------------------------------------ + * + * Constants and Enums + * + *----------------------------------------------------------------------------*/ + + private static final Logger logger = Logger.getLogger(RestyWrapper.class.getName()); + +/*------------------------------------------------------------------------------ + * + * Internal State + * + *----------------------------------------------------------------------------*/ + + private static CircularBuffer> timestamps = new CircularBuffer<>(1000); + + private Resty resty; + private List> rateLimits; + + +/*============================================================================== + * ------- ------- + * ------- Public Interface To This Class ------- + * ------- ------- + *============================================================================*/ + +/*------------------------------------------------------------------------------ + * + * Constructors + * + *----------------------------------------------------------------------------*/ + + public RestyWrapper(int readTimeout, List> limits) { + if (limits == null) { + rateLimits = new ArrayList<>(); + rateLimits.add(new Pair<>(10, 10)); // No more than 10 requests in 10 seconds + rateLimits.add(new Pair<>(20, 60)); // No more than 20 requests/minute + rateLimits.add(new Pair<>(600, 60*60)); // No more than 600 requests/hour + } else { + rateLimits = limits; + } + if (readTimeout <= 0) { + resty = new Resty(); + } else { + resty = new Resty(new ReadTimeoutOption(readTimeout)); + } + } + + public RestyWrapper() { + this(-1, null); + } + + public RestyWrapper(List> limits) { + this(-1, limits); + } + + public RestyWrapper(int readTimeout) { + this(readTimeout, null); + } + +/*------------------------------------------------------------------------------ + * + * REST API Invocations + * + *----------------------------------------------------------------------------*/ + + public TextResource text(String anUri) throws IOException { + startRequest(anUri); + return resty.text(anUri); + } + + public TextResource text(String anUri, AbstractContent content) throws IOException { + startRequest(anUri); + return resty.text(anUri, content); + } + + public JSONResource json(String anUri) throws IOException { + startRequest(anUri); + return resty.json(anUri); + } + +/*------------------------------------------------------------------------------ + * + * Static Utility Functions + * + *----------------------------------------------------------------------------*/ + + public static FormContent form(String query) { + return Resty.form(query); + } + + public static String enc(String unencodedString) { + return Resty.enc(unencodedString); + } + + public void withHeader(String aHeader, String aValue) { + resty.withHeader(aHeader, aValue); + } + + +/*------------------------------------------------------------------------------ + * + * PRIVATE - Utility Classes and Methods + * + *----------------------------------------------------------------------------*/ + + private class ReadTimeoutOption extends Resty.Option { + private int timeout; + + ReadTimeoutOption(int timeout) { this.timeout = timeout; } + + public void apply(URLConnection aConnection) { + aConnection.setReadTimeout(timeout); + } + } + + private void startRequest(String endpoint) { + timestamps.insert(new Pair<>(System.currentTimeMillis(), endpoint)); + while (rateLimit(endpoint)) { + Utils.sleep(5 * 1000); + } + } + + private boolean rateLimit(String endpoint) { + long now = System.currentTimeMillis(); + int size = timestamps.size(); + + for (Pair limit : rateLimits) { + int count = limit.item1; + int seconds = limit.item2; + if (size < count) return false; + + Pair p = timestamps.peekAt(size - count); + long nthRequest = p.item1; + + if ((now - nthRequest) < seconds * 1000) { + logger.log( + Level.INFO, "Throttling: More than {0} requests in {1} seconds - {2}", + new Object[]{count, seconds, endpoint}); + return true; + } + } + + return false; + } + +} diff --git a/src/org/noroomattheinn/utils/Utils.java b/src/org/noroomattheinn/utils/Utils.java index 42a13b6..9004d70 100644 --- a/src/org/noroomattheinn/utils/Utils.java +++ b/src/org/noroomattheinn/utils/Utils.java @@ -6,7 +6,6 @@ package org.noroomattheinn.utils; -import java.util.Comparator; import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -128,9 +127,13 @@ public static double percentChange(double oldValue, double newValue) { } public static void sleep(long timeInMillis) { - try { - Thread.sleep(timeInMillis); - } catch (InterruptedException ex) { } + long initialTime = System.currentTimeMillis(); + try { Thread.sleep(500); } catch (InterruptedException ex) { } + while (System.currentTimeMillis() - initialTime < timeInMillis) { + try { + Thread.sleep(500); + } catch (InterruptedException ex) { return; } + } } public static int compareVersions(String versionA, String versionB) { @@ -147,4 +150,7 @@ public static int compareVersions(String versionA, String versionB) { return partsOfA.length - partsOfB.length; } + public interface Callback { + public R call(P parameter); + } }