From c4b37876604e233243b4b95284af6251ba2be6ea Mon Sep 17 00:00:00 2001 From: Tabs Date: Mon, 28 Nov 2016 13:22:27 -0500 Subject: [PATCH 01/15] support user events in event log, minor bug fixes for enterprise log inconsistencies --- src/main/java/com/box/sdk/EventLog.java | 61 +++++++++++++++++++++---- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/box/sdk/EventLog.java b/src/main/java/com/box/sdk/EventLog.java index 618867f4c..089135fdd 100644 --- a/src/main/java/com/box/sdk/EventLog.java +++ b/src/main/java/com/box/sdk/EventLog.java @@ -22,20 +22,28 @@ public class EventLog implements Iterable { private static final int ENTERPRISE_LIMIT = 500; private static final URLTemplate ENTERPRISE_EVENT_URL_TEMPLATE = new URLTemplate("events?stream_type=admin_logs&" + "limit=" + ENTERPRISE_LIMIT); + private static final int USER_LIMIT = 800; + private static final URLTemplate USER_EVENT_URL_TEMPLATE = new URLTemplate("events?limit=" + USER_LIMIT + "&stream_position=%s"); + public static final long STREAM_POSITION_NOW = -1; private final int chunkSize; private final int limit; - private final String nextStreamPosition; - private final String streamPosition; + private final long nextStreamPosition; + private final long streamPosition; private final Set set; private Date startDate; private Date endDate; - EventLog(BoxAPIConnection api, JsonObject json, String streamPosition, int limit) { + EventLog(BoxAPIConnection api, JsonObject json, long streamPosition, int limit) { this.streamPosition = streamPosition; this.limit = limit; - this.nextStreamPosition = json.get("next_stream_position").asString(); + JsonValue position = json.get("next_stream_position"); + if(position.isString()) { + this.nextStreamPosition = Long.valueOf(position.asString()); + } else { + this.nextStreamPosition = position.asLong(); + } this.chunkSize = json.get("chunk_size").asInt(); this.set = new LinkedHashSet(this.chunkSize); @@ -54,7 +62,7 @@ public class EventLog implements Iterable { * @return a log of all the events that met the given criteria. */ public static EventLog getEnterpriseEvents(BoxAPIConnection api, Date after, Date before, BoxEvent.Type... types) { - return getEnterpriseEvents(api, null, after, before, types); + return getEnterpriseEvents(api, STREAM_POSITION_NOW, after, before, types); } /** @@ -67,12 +75,12 @@ public static EventLog getEnterpriseEvents(BoxAPIConnection api, Date after, Dat * @param types an optional list of event types to filter by. * @return a log of all the events that met the given criteria. */ - public static EventLog getEnterpriseEvents(BoxAPIConnection api, String position, Date after, Date before, + public static EventLog getEnterpriseEvents(BoxAPIConnection api, long position, Date after, Date before, BoxEvent.Type... types) { URL url = ENTERPRISE_EVENT_URL_TEMPLATE.build(api.getBaseURL()); - if (position != null || types.length > 0 || after != null + if (types.length > 0 || after != null || before != null) { QueryStringBuilder queryBuilder = new QueryStringBuilder(url.getQuery()); @@ -86,7 +94,7 @@ public static EventLog getEnterpriseEvents(BoxAPIConnection api, String position BoxDateFormat.format(before)); } - if (position != null) { + if (position != STREAM_POSITION_NOW) { queryBuilder.appendParam("stream_position", position); } @@ -178,7 +186,7 @@ public int getLimit() { * * @return the starting position within the event stream. */ - public String getStreamPosition() { + public long getStreamPosition() { return this.streamPosition; } @@ -190,7 +198,7 @@ public String getStreamPosition() { * * @return the next position within the event stream. */ - public String getNextStreamPosition() { + public long getNextStreamPosition() { return this.nextStreamPosition; } @@ -219,4 +227,37 @@ public int getChunkSize() { public int getSize() { return this.set.size(); } + + public static EventLog getUserEvents(BoxAPIConnection api, long position, BoxEvent.Type... types) { + if (position == STREAM_POSITION_NOW) { + BoxAPIRequest request = new BoxAPIRequest(api, USER_EVENT_URL_TEMPLATE.build(api.getBaseURL(), "now"), "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); + position = jsonObject.get("next_stream_position").asLong(); + } + + URL url = USER_EVENT_URL_TEMPLATE.build(api.getBaseURL(), position); + + QueryStringBuilder queryBuilder = new QueryStringBuilder(url.getQuery()); + + StringBuilder filterBuilder = new StringBuilder(); + for (BoxEvent.Type filterType : types) { + filterBuilder.append(filterType.name()); + filterBuilder.append(','); + } + filterBuilder.deleteCharAt(filterBuilder.length() - 1); + queryBuilder.appendParam("event_type", filterBuilder.toString()); + + try { + url = queryBuilder.addToURL(url); + } catch (MalformedURLException e) { + throw new BoxAPIException("Couldn't append a query string to the provided URL."); + } + + BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); + EventLog log = new EventLog(api, responseJSON, position, USER_LIMIT); + return log; + } } From 86427df793b432ea7b743b0bfa85326b82455f90 Mon Sep 17 00:00:00 2001 From: Tabs Date: Tue, 29 Nov 2016 21:54:56 -0500 Subject: [PATCH 02/15] add auth refres into api request handling --- .../java/com/box/sdk/BoxAPIConnection.java | 2 +- src/main/java/com/box/sdk/BoxAPIRequest.java | 8 +++- .../box/sdk/ReloadableBoxAPIConnection.java | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index ff8b5f4d8..5d17ea684 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -47,7 +47,7 @@ public class BoxAPIConnection { private String proxyPassword; private String userAgent; - private String accessToken; + protected String accessToken; private String refreshToken; private String tokenURL; private String baseURL; diff --git a/src/main/java/com/box/sdk/BoxAPIRequest.java b/src/main/java/com/box/sdk/BoxAPIRequest.java index 11e779cfa..5d3f513b6 100644 --- a/src/main/java/com/box/sdk/BoxAPIRequest.java +++ b/src/main/java/com/box/sdk/BoxAPIRequest.java @@ -208,7 +208,11 @@ public BoxAPIResponse send(ProgressListener listener) { try { return this.trySend(listener); } catch (BoxAPIException apiException) { - if (!this.backoffCounter.decrement() || !isResponseRetryable(apiException.getResponseCode())) { + this.backoffCounter.decrement(); + + if (this.backoffCounter.getAttemptsRemaining() > 0 && this.api.canRefresh() && isResponseUnauthorized(apiException.getResponseCode())) { + this.api.refresh(); + } else if (this.backoffCounter.getAttemptsRemaining() < 1 || !isResponseRetryable(apiException.getResponseCode())) { throw apiException; } @@ -515,6 +519,8 @@ private static boolean isResponseRedirect(int responseCode) { return (responseCode == 301 || responseCode == 302); } + private static boolean isResponseUnauthorized(int responseCode) { return responseCode == 401; } + private final class RequestHeader { private final String key; private final String value; diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java new file mode 100644 index 000000000..896a61503 --- /dev/null +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -0,0 +1,42 @@ +package com.box.sdk; + +import com.eclipsesource.json.JsonObject; + +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Represents an authenticated connection to the Box API. + * + *

This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be + * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of + * BoxAPIConnection may be created to support multi-user login.

+ */ +public class ReloadableBoxAPIConnection extends BoxAPIConnection { + /** + * Constructs a new ReloadableBoxAPIConnection with an access token that can be refreshed. + * @param clientID the client ID to use when refreshing the access token. + * @param clientSecret the client secret to use when refreshing the access token. + * @param accessToken an initial access token to use for authenticating with the API. + * @param refreshToken an initial refresh token to use when refreshing the access token. + */ + public ReloadableBoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) { + super(clientID, clientSecret, accessToken, refreshToken); + } + + /** + * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the + * access token if it has expired since the last call to getAccessToken(). + * @return a valid access token that can be used to authenticate an API request. + */ + public String getAccessToken() { return this.accessToken; } + + public boolean needsRefresh() { + return false; + } +} From 12e5968a780e7554d3d38d2d2ca8f8913fc61571 Mon Sep 17 00:00:00 2001 From: Tabs Date: Fri, 23 Dec 2016 13:25:52 -0500 Subject: [PATCH 03/15] user event log do not take a types parameter --- src/main/java/com/box/sdk/EventLog.java | 10 +-------- .../box/sdk/ReloadableBoxAPIConnection.java | 21 ++++--------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/box/sdk/EventLog.java b/src/main/java/com/box/sdk/EventLog.java index 089135fdd..5bd53ea29 100644 --- a/src/main/java/com/box/sdk/EventLog.java +++ b/src/main/java/com/box/sdk/EventLog.java @@ -228,7 +228,7 @@ public int getSize() { return this.set.size(); } - public static EventLog getUserEvents(BoxAPIConnection api, long position, BoxEvent.Type... types) { + public static EventLog getUserEvents(BoxAPIConnection api, long position) { if (position == STREAM_POSITION_NOW) { BoxAPIRequest request = new BoxAPIRequest(api, USER_EVENT_URL_TEMPLATE.build(api.getBaseURL(), "now"), "GET"); BoxJSONResponse response = (BoxJSONResponse) request.send(); @@ -240,14 +240,6 @@ public static EventLog getUserEvents(BoxAPIConnection api, long position, BoxEve QueryStringBuilder queryBuilder = new QueryStringBuilder(url.getQuery()); - StringBuilder filterBuilder = new StringBuilder(); - for (BoxEvent.Type filterType : types) { - filterBuilder.append(filterType.name()); - filterBuilder.append(','); - } - filterBuilder.deleteCharAt(filterBuilder.length() - 1); - queryBuilder.appendParam("event_type", filterBuilder.toString()); - try { url = queryBuilder.addToURL(url); } catch (MalformedURLException e) { diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java index 896a61503..5719d9fbd 100644 --- a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -1,19 +1,9 @@ package com.box.sdk; -import com.eclipsesource.json.JsonObject; - -import java.net.MalformedURLException; -import java.net.Proxy; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - /** * Represents an authenticated connection to the Box API. * - *

This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be + *

This class handles storing authentication information, token refresh, and rate-limiting. It can also be * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of * BoxAPIConnection may be created to support multi-user login.

*/ @@ -30,13 +20,10 @@ public ReloadableBoxAPIConnection(String clientID, String clientSecret, String a } /** - * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the - * access token if it has expired since the last call to getAccessToken(). - * @return a valid access token that can be used to authenticate an API request. + * Gets an access token that can be used to authenticate an API request. + * @return an access token that can be used to authenticate an API request. */ public String getAccessToken() { return this.accessToken; } - public boolean needsRefresh() { - return false; - } + public boolean needsRefresh() { return false; } } From 6e460071331b4bb844eb1f6c3d7d93420f25fc13 Mon Sep 17 00:00:00 2001 From: Ryan Cooke Date: Wed, 8 Mar 2017 12:03:31 -0500 Subject: [PATCH 04/15] Adds a BoxAPIConnectionListener method for determining whether a BoxAPIConnection can refresh its tokens --- src/main/java/com/box/sdk/BoxAPIConnection.java | 2 +- .../java/com/box/sdk/BoxAPIConnectionListener.java | 13 +++++++++++++ .../com/box/sdk/ReloadableBoxAPIConnection.java | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index 5d17ea684..a1f1ecacf 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -54,7 +54,7 @@ public class BoxAPIConnection { private String baseUploadURL; private boolean autoRefresh; private int maxRequestAttempts; - private List listeners; + protected List listeners; private RequestInterceptor interceptor; /** diff --git a/src/main/java/com/box/sdk/BoxAPIConnectionListener.java b/src/main/java/com/box/sdk/BoxAPIConnectionListener.java index a30395815..c30642ee3 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnectionListener.java +++ b/src/main/java/com/box/sdk/BoxAPIConnectionListener.java @@ -5,6 +5,19 @@ */ public interface BoxAPIConnectionListener { + /** + * Called before the Box API connection refreshes its tokens. + * It can short circuit the refresh if the listener returns false. + * This is useful for testing the availability of a persistence resource such as a database. + * + *

The provided connection is guaranteed to not be auto-refreshed or modified by another listener until this + * method returns.

+ * + * @param api the API connection that was refreshed. + * @return a boolean indicating whether the refresh should proceed + */ + default boolean preRefresh(BoxAPIConnection api) { return true; }; + /** * Called when the Box API connection refreshes its tokens. * diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java index 5719d9fbd..276746da4 100644 --- a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -23,7 +23,20 @@ public ReloadableBoxAPIConnection(String clientID, String clientSecret, String a * Gets an access token that can be used to authenticate an API request. * @return an access token that can be used to authenticate an API request. */ + @Override public String getAccessToken() { return this.accessToken; } + @Override + public boolean canRefresh() { + if(super.canRefresh()) { + for(BoxAPIConnectionListener listener: listeners) + if (!listener.preRefresh(this)) + return false; + return true; + } + return false; + } + + @Override public boolean needsRefresh() { return false; } } From c66141fe810f4e525e26ff68d51f1d6523449638 Mon Sep 17 00:00:00 2001 From: Ryan Cooke Date: Wed, 8 Mar 2017 19:11:21 -0500 Subject: [PATCH 05/15] Don't acquire the refresh lock if can't refresh, and don't raise exceptions when you can notify errors --- src/main/java/com/box/sdk/BoxAPIConnection.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index a1f1ecacf..97f79c85b 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -435,14 +435,14 @@ public boolean needsRefresh() { * @throws IllegalStateException if this connection's access token cannot be refreshed. */ public void refresh() { - this.refreshLock.writeLock().lock(); - if (!this.canRefresh()) { - this.refreshLock.writeLock().unlock(); - throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a " - + "refresh token."); + BoxAPIException e = new BoxAPIException("The BoxAPIConnection cannot be refreshed because canRefresh check failed."); + this.notifyError(e); + return; } + this.refreshLock.writeLock().lock(); + URL url = null; try { url = new URL(this.tokenURL); @@ -466,7 +466,7 @@ public void refresh() { } catch (BoxAPIException e) { this.notifyError(e); this.refreshLock.writeLock().unlock(); - throw e; + return; } JsonObject jsonObject = JsonObject.readFrom(json); From 454007046e7181b178e364d00a4577fa39f979b3 Mon Sep 17 00:00:00 2001 From: Ryan Cooke Date: Thu, 9 Mar 2017 20:15:58 -0500 Subject: [PATCH 06/15] canRefresh() happens to be called in a lot of situations, and is not a suitable place to invoke preRefresh --- .../com/box/sdk/ReloadableBoxAPIConnection.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java index 276746da4..3cf869ba6 100644 --- a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -27,14 +27,12 @@ public ReloadableBoxAPIConnection(String clientID, String clientSecret, String a public String getAccessToken() { return this.accessToken; } @Override - public boolean canRefresh() { - if(super.canRefresh()) { - for(BoxAPIConnectionListener listener: listeners) - if (!listener.preRefresh(this)) - return false; - return true; - } - return false; + public void refresh() { + for(BoxAPIConnectionListener listener: listeners) + if (!listener.preRefresh(this)) + return; + + super.refresh(); } @Override From 5f155ef6ad3b7ea7f803011a29485a6d89926c1e Mon Sep 17 00:00:00 2001 From: Tabs Date: Mon, 28 Nov 2016 13:22:27 -0500 Subject: [PATCH 07/15] support user events in event log, minor bug fixes for enterprise log inconsistencies --- src/main/java/com/box/sdk/EventLog.java | 61 +++++++++++++++++++++---- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/box/sdk/EventLog.java b/src/main/java/com/box/sdk/EventLog.java index 618867f4c..089135fdd 100644 --- a/src/main/java/com/box/sdk/EventLog.java +++ b/src/main/java/com/box/sdk/EventLog.java @@ -22,20 +22,28 @@ public class EventLog implements Iterable { private static final int ENTERPRISE_LIMIT = 500; private static final URLTemplate ENTERPRISE_EVENT_URL_TEMPLATE = new URLTemplate("events?stream_type=admin_logs&" + "limit=" + ENTERPRISE_LIMIT); + private static final int USER_LIMIT = 800; + private static final URLTemplate USER_EVENT_URL_TEMPLATE = new URLTemplate("events?limit=" + USER_LIMIT + "&stream_position=%s"); + public static final long STREAM_POSITION_NOW = -1; private final int chunkSize; private final int limit; - private final String nextStreamPosition; - private final String streamPosition; + private final long nextStreamPosition; + private final long streamPosition; private final Set set; private Date startDate; private Date endDate; - EventLog(BoxAPIConnection api, JsonObject json, String streamPosition, int limit) { + EventLog(BoxAPIConnection api, JsonObject json, long streamPosition, int limit) { this.streamPosition = streamPosition; this.limit = limit; - this.nextStreamPosition = json.get("next_stream_position").asString(); + JsonValue position = json.get("next_stream_position"); + if(position.isString()) { + this.nextStreamPosition = Long.valueOf(position.asString()); + } else { + this.nextStreamPosition = position.asLong(); + } this.chunkSize = json.get("chunk_size").asInt(); this.set = new LinkedHashSet(this.chunkSize); @@ -54,7 +62,7 @@ public class EventLog implements Iterable { * @return a log of all the events that met the given criteria. */ public static EventLog getEnterpriseEvents(BoxAPIConnection api, Date after, Date before, BoxEvent.Type... types) { - return getEnterpriseEvents(api, null, after, before, types); + return getEnterpriseEvents(api, STREAM_POSITION_NOW, after, before, types); } /** @@ -67,12 +75,12 @@ public static EventLog getEnterpriseEvents(BoxAPIConnection api, Date after, Dat * @param types an optional list of event types to filter by. * @return a log of all the events that met the given criteria. */ - public static EventLog getEnterpriseEvents(BoxAPIConnection api, String position, Date after, Date before, + public static EventLog getEnterpriseEvents(BoxAPIConnection api, long position, Date after, Date before, BoxEvent.Type... types) { URL url = ENTERPRISE_EVENT_URL_TEMPLATE.build(api.getBaseURL()); - if (position != null || types.length > 0 || after != null + if (types.length > 0 || after != null || before != null) { QueryStringBuilder queryBuilder = new QueryStringBuilder(url.getQuery()); @@ -86,7 +94,7 @@ public static EventLog getEnterpriseEvents(BoxAPIConnection api, String position BoxDateFormat.format(before)); } - if (position != null) { + if (position != STREAM_POSITION_NOW) { queryBuilder.appendParam("stream_position", position); } @@ -178,7 +186,7 @@ public int getLimit() { * * @return the starting position within the event stream. */ - public String getStreamPosition() { + public long getStreamPosition() { return this.streamPosition; } @@ -190,7 +198,7 @@ public String getStreamPosition() { * * @return the next position within the event stream. */ - public String getNextStreamPosition() { + public long getNextStreamPosition() { return this.nextStreamPosition; } @@ -219,4 +227,37 @@ public int getChunkSize() { public int getSize() { return this.set.size(); } + + public static EventLog getUserEvents(BoxAPIConnection api, long position, BoxEvent.Type... types) { + if (position == STREAM_POSITION_NOW) { + BoxAPIRequest request = new BoxAPIRequest(api, USER_EVENT_URL_TEMPLATE.build(api.getBaseURL(), "now"), "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); + position = jsonObject.get("next_stream_position").asLong(); + } + + URL url = USER_EVENT_URL_TEMPLATE.build(api.getBaseURL(), position); + + QueryStringBuilder queryBuilder = new QueryStringBuilder(url.getQuery()); + + StringBuilder filterBuilder = new StringBuilder(); + for (BoxEvent.Type filterType : types) { + filterBuilder.append(filterType.name()); + filterBuilder.append(','); + } + filterBuilder.deleteCharAt(filterBuilder.length() - 1); + queryBuilder.appendParam("event_type", filterBuilder.toString()); + + try { + url = queryBuilder.addToURL(url); + } catch (MalformedURLException e) { + throw new BoxAPIException("Couldn't append a query string to the provided URL."); + } + + BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); + EventLog log = new EventLog(api, responseJSON, position, USER_LIMIT); + return log; + } } From b9b8c44165d8acf5d091dbab2b956a4697948ff6 Mon Sep 17 00:00:00 2001 From: Tabs Date: Tue, 29 Nov 2016 21:54:56 -0500 Subject: [PATCH 08/15] add auth refres into api request handling --- .../java/com/box/sdk/BoxAPIConnection.java | 2 +- src/main/java/com/box/sdk/BoxAPIRequest.java | 8 +++- .../box/sdk/ReloadableBoxAPIConnection.java | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index 1912a5df3..40a883899 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -49,7 +49,7 @@ public class BoxAPIConnection { private String proxyPassword; private String userAgent; - private String accessToken; + protected String accessToken; private String refreshToken; private String tokenURL; private String baseURL; diff --git a/src/main/java/com/box/sdk/BoxAPIRequest.java b/src/main/java/com/box/sdk/BoxAPIRequest.java index 036da1671..7d6d888b1 100644 --- a/src/main/java/com/box/sdk/BoxAPIRequest.java +++ b/src/main/java/com/box/sdk/BoxAPIRequest.java @@ -220,7 +220,11 @@ public BoxAPIResponse send(ProgressListener listener) { try { return this.trySend(listener); } catch (BoxAPIException apiException) { - if (!this.backoffCounter.decrement() || !isResponseRetryable(apiException.getResponseCode())) { + this.backoffCounter.decrement(); + + if (this.backoffCounter.getAttemptsRemaining() > 0 && this.api.canRefresh() && isResponseUnauthorized(apiException.getResponseCode())) { + this.api.refresh(); + } else if (this.backoffCounter.getAttemptsRemaining() < 1 || !isResponseRetryable(apiException.getResponseCode())) { throw apiException; } @@ -527,6 +531,8 @@ private static boolean isResponseRedirect(int responseCode) { return (responseCode == 301 || responseCode == 302); } + private static boolean isResponseUnauthorized(int responseCode) { return responseCode == 401; } + private final class RequestHeader { private final String key; private final String value; diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java new file mode 100644 index 000000000..896a61503 --- /dev/null +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -0,0 +1,42 @@ +package com.box.sdk; + +import com.eclipsesource.json.JsonObject; + +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Represents an authenticated connection to the Box API. + * + *

This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be + * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of + * BoxAPIConnection may be created to support multi-user login.

+ */ +public class ReloadableBoxAPIConnection extends BoxAPIConnection { + /** + * Constructs a new ReloadableBoxAPIConnection with an access token that can be refreshed. + * @param clientID the client ID to use when refreshing the access token. + * @param clientSecret the client secret to use when refreshing the access token. + * @param accessToken an initial access token to use for authenticating with the API. + * @param refreshToken an initial refresh token to use when refreshing the access token. + */ + public ReloadableBoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) { + super(clientID, clientSecret, accessToken, refreshToken); + } + + /** + * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the + * access token if it has expired since the last call to getAccessToken(). + * @return a valid access token that can be used to authenticate an API request. + */ + public String getAccessToken() { return this.accessToken; } + + public boolean needsRefresh() { + return false; + } +} From d44ae1f72797c4f26b6f08e18fe1d804d5488ed6 Mon Sep 17 00:00:00 2001 From: Tabs Date: Fri, 23 Dec 2016 13:25:52 -0500 Subject: [PATCH 09/15] user event log do not take a types parameter --- src/main/java/com/box/sdk/EventLog.java | 10 +-------- .../box/sdk/ReloadableBoxAPIConnection.java | 21 ++++--------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/box/sdk/EventLog.java b/src/main/java/com/box/sdk/EventLog.java index 089135fdd..5bd53ea29 100644 --- a/src/main/java/com/box/sdk/EventLog.java +++ b/src/main/java/com/box/sdk/EventLog.java @@ -228,7 +228,7 @@ public int getSize() { return this.set.size(); } - public static EventLog getUserEvents(BoxAPIConnection api, long position, BoxEvent.Type... types) { + public static EventLog getUserEvents(BoxAPIConnection api, long position) { if (position == STREAM_POSITION_NOW) { BoxAPIRequest request = new BoxAPIRequest(api, USER_EVENT_URL_TEMPLATE.build(api.getBaseURL(), "now"), "GET"); BoxJSONResponse response = (BoxJSONResponse) request.send(); @@ -240,14 +240,6 @@ public static EventLog getUserEvents(BoxAPIConnection api, long position, BoxEve QueryStringBuilder queryBuilder = new QueryStringBuilder(url.getQuery()); - StringBuilder filterBuilder = new StringBuilder(); - for (BoxEvent.Type filterType : types) { - filterBuilder.append(filterType.name()); - filterBuilder.append(','); - } - filterBuilder.deleteCharAt(filterBuilder.length() - 1); - queryBuilder.appendParam("event_type", filterBuilder.toString()); - try { url = queryBuilder.addToURL(url); } catch (MalformedURLException e) { diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java index 896a61503..5719d9fbd 100644 --- a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -1,19 +1,9 @@ package com.box.sdk; -import com.eclipsesource.json.JsonObject; - -import java.net.MalformedURLException; -import java.net.Proxy; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - /** * Represents an authenticated connection to the Box API. * - *

This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be + *

This class handles storing authentication information, token refresh, and rate-limiting. It can also be * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of * BoxAPIConnection may be created to support multi-user login.

*/ @@ -30,13 +20,10 @@ public ReloadableBoxAPIConnection(String clientID, String clientSecret, String a } /** - * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the - * access token if it has expired since the last call to getAccessToken(). - * @return a valid access token that can be used to authenticate an API request. + * Gets an access token that can be used to authenticate an API request. + * @return an access token that can be used to authenticate an API request. */ public String getAccessToken() { return this.accessToken; } - public boolean needsRefresh() { - return false; - } + public boolean needsRefresh() { return false; } } From 8d2827fe79bb8284efb919a68a5bc0dc93cdcbc3 Mon Sep 17 00:00:00 2001 From: Ryan Cooke Date: Wed, 8 Mar 2017 12:03:31 -0500 Subject: [PATCH 10/15] Adds a BoxAPIConnectionListener method for determining whether a BoxAPIConnection can refresh its tokens --- src/main/java/com/box/sdk/BoxAPIConnection.java | 2 +- .../java/com/box/sdk/BoxAPIConnectionListener.java | 13 +++++++++++++ .../com/box/sdk/ReloadableBoxAPIConnection.java | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index 40a883899..f09b9f98c 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -56,7 +56,7 @@ public class BoxAPIConnection { private String baseUploadURL; private boolean autoRefresh; private int maxRequestAttempts; - private List listeners; + protected List listeners; private RequestInterceptor interceptor; /** diff --git a/src/main/java/com/box/sdk/BoxAPIConnectionListener.java b/src/main/java/com/box/sdk/BoxAPIConnectionListener.java index a30395815..c30642ee3 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnectionListener.java +++ b/src/main/java/com/box/sdk/BoxAPIConnectionListener.java @@ -5,6 +5,19 @@ */ public interface BoxAPIConnectionListener { + /** + * Called before the Box API connection refreshes its tokens. + * It can short circuit the refresh if the listener returns false. + * This is useful for testing the availability of a persistence resource such as a database. + * + *

The provided connection is guaranteed to not be auto-refreshed or modified by another listener until this + * method returns.

+ * + * @param api the API connection that was refreshed. + * @return a boolean indicating whether the refresh should proceed + */ + default boolean preRefresh(BoxAPIConnection api) { return true; }; + /** * Called when the Box API connection refreshes its tokens. * diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java index 5719d9fbd..276746da4 100644 --- a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -23,7 +23,20 @@ public ReloadableBoxAPIConnection(String clientID, String clientSecret, String a * Gets an access token that can be used to authenticate an API request. * @return an access token that can be used to authenticate an API request. */ + @Override public String getAccessToken() { return this.accessToken; } + @Override + public boolean canRefresh() { + if(super.canRefresh()) { + for(BoxAPIConnectionListener listener: listeners) + if (!listener.preRefresh(this)) + return false; + return true; + } + return false; + } + + @Override public boolean needsRefresh() { return false; } } From d9dc6e6af8db6066c76b0bf3adddb43513cc8be6 Mon Sep 17 00:00:00 2001 From: Ryan Cooke Date: Wed, 8 Mar 2017 19:11:21 -0500 Subject: [PATCH 11/15] Don't acquire the refresh lock if can't refresh, and don't raise exceptions when you can notify errors --- src/main/java/com/box/sdk/BoxAPIConnection.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index f09b9f98c..aa879fe42 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -471,14 +471,14 @@ public boolean needsRefresh() { * @throws IllegalStateException if this connection's access token cannot be refreshed. */ public void refresh() { - this.refreshLock.writeLock().lock(); - if (!this.canRefresh()) { - this.refreshLock.writeLock().unlock(); - throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a " - + "refresh token."); + BoxAPIException e = new BoxAPIException("The BoxAPIConnection cannot be refreshed because canRefresh check failed."); + this.notifyError(e); + return; } + this.refreshLock.writeLock().lock(); + URL url = null; try { url = new URL(this.tokenURL); @@ -502,7 +502,7 @@ public void refresh() { } catch (BoxAPIException e) { this.notifyError(e); this.refreshLock.writeLock().unlock(); - throw e; + return; } JsonObject jsonObject = JsonObject.readFrom(json); From aeb6191bef851ff44e3507eb2586fabc8de9fce9 Mon Sep 17 00:00:00 2001 From: Ryan Cooke Date: Thu, 9 Mar 2017 20:15:58 -0500 Subject: [PATCH 12/15] canRefresh() happens to be called in a lot of situations, and is not a suitable place to invoke preRefresh --- .../com/box/sdk/ReloadableBoxAPIConnection.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java index 276746da4..3cf869ba6 100644 --- a/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -27,14 +27,12 @@ public ReloadableBoxAPIConnection(String clientID, String clientSecret, String a public String getAccessToken() { return this.accessToken; } @Override - public boolean canRefresh() { - if(super.canRefresh()) { - for(BoxAPIConnectionListener listener: listeners) - if (!listener.preRefresh(this)) - return false; - return true; - } - return false; + public void refresh() { + for(BoxAPIConnectionListener listener: listeners) + if (!listener.preRefresh(this)) + return; + + super.refresh(); } @Override From 36106b86820d68f027c13bd7009df62d390a5ce1 Mon Sep 17 00:00:00 2001 From: ACE Date: Mon, 17 Apr 2017 15:49:17 -0400 Subject: [PATCH 13/15] Support for new box event --- src/main/java/com/box/sdk/BoxEvent.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxEvent.java b/src/main/java/com/box/sdk/BoxEvent.java index c03332710..b772ab869 100644 --- a/src/main/java/com/box/sdk/BoxEvent.java +++ b/src/main/java/com/box/sdk/BoxEvent.java @@ -219,6 +219,11 @@ public enum Type { */ ITEM_PREVIEW, + /** + * A representation of a file was accessed. This may include preview or offline access. + */ + CONTENT_ACCESS, + /** * A file or folder was moved. */ From c0eaf8046ad0b41aad151ef779c5e06dbd2b930c Mon Sep 17 00:00:00 2001 From: ACE Date: Mon, 15 May 2017 10:59:44 -0400 Subject: [PATCH 14/15] Support for grabbing raw event --- src/main/java/com/box/sdk/BoxEvent.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxEvent.java b/src/main/java/com/box/sdk/BoxEvent.java index b772ab869..bc082ac08 100644 --- a/src/main/java/com/box/sdk/BoxEvent.java +++ b/src/main/java/com/box/sdk/BoxEvent.java @@ -20,6 +20,7 @@ public class BoxEvent extends BoxResource { private BoxCollaborator.Info accessibleBy; private BoxUser.Info createdBy; private String sessionID; + private JsonObject rawEvent; /** * Constructs a BoxEvent from a JSON string. @@ -33,6 +34,8 @@ public BoxEvent(BoxAPIConnection api, String json) { BoxEvent(BoxAPIConnection api, JsonObject jsonObject) { super(api, jsonObject.get("event_id").asString()); + this.rawEvent = jsonObject; + for (JsonObject.Member member : jsonObject) { if (member.getValue().isNull()) { continue; @@ -42,6 +45,14 @@ public BoxEvent(BoxAPIConnection api, String json) { } } + /** + * Gets the event as it is returned from the Box API + * @return the raw event from the Box API + */ + public JsonObject getRawEvent() { + return rawEvent; + } + /** * Gets info about the source of this event. * From b22e936e64ebdb7de662c7ea0997fd01e1e2a260 Mon Sep 17 00:00:00 2001 From: Ryan Cooke Date: Fri, 20 Oct 2017 16:11:33 -0400 Subject: [PATCH 15/15] add build sbt file --- build.sbt | 19 +++++++++++++++++++ project/plugins.sbt | 1 + 2 files changed, 20 insertions(+) create mode 100644 build.sbt create mode 100644 project/plugins.sbt diff --git a/build.sbt b/build.sbt new file mode 100644 index 000000000..313f335e9 --- /dev/null +++ b/build.sbt @@ -0,0 +1,19 @@ +name := "box-java-sdk" +organization := "com.box" +version := "2.3.2" +scalaVersion := "2.11.8" + +resolvers in ThisBuild ++= Seq("BoxJavaSdk" at "s3://docurated-build/boxjavasdk") + +publishTo := Some("BoxJavaSdk" at "s3://docurated-build/boxjavasdk") + +libraryDependencies += "com.eclipsesource.minimal-json" % "minimal-json" % "0.9.1" +libraryDependencies += "org.bitbucket.b_c" % "jose4j" % "0.4.4" +libraryDependencies += "org.bouncycastle" % "bcprov-jdk15on" % "1.52" +libraryDependencies += "org.bouncycastle" % "bcpkix-jdk15on" % "1.52" +libraryDependencies += "junit" % "junit" % "4.11" % "test" +libraryDependencies += "org.hamcrest" % "hamcrest-library" % "1.3" % "test" +libraryDependencies += "com.github.tomakehurst" % "wiremock" % "1.52" % "test" +libraryDependencies += "org.mockito" % "mockito-core" % "1.9.5" % "test" +libraryDependencies += "org.slf4j" % "slf4j-api" % "1.7.7" % "test" +libraryDependencies += "org.slf4j" % "slf4j-nop" % "1.7.7" % "test" diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 000000000..8a0fa2b4c --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.frugalmechanic" % "fm-sbt-s3-resolver" % "0.9.0")