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") diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index 1912a5df3..aa879fe42 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -49,14 +49,14 @@ public class BoxAPIConnection { private String proxyPassword; private String userAgent; - private String accessToken; + protected String accessToken; private String refreshToken; private String tokenURL; private String baseURL; private String baseUploadURL; private boolean autoRefresh; private int maxRequestAttempts; - private List listeners; + protected List listeners; private RequestInterceptor interceptor; /** @@ -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); 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/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/BoxEvent.java b/src/main/java/com/box/sdk/BoxEvent.java index c03332710..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. * @@ -219,6 +230,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. */ diff --git a/src/main/java/com/box/sdk/EventLog.java b/src/main/java/com/box/sdk/EventLog.java index 618867f4c..5bd53ea29 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,29 @@ public int getChunkSize() { public int getSize() { return this.set.size(); } + + 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(); + 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()); + + 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; + } } 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..3cf869ba6 --- /dev/null +++ b/src/main/java/com/box/sdk/ReloadableBoxAPIConnection.java @@ -0,0 +1,40 @@ +package com.box.sdk; + +/** + * Represents an authenticated connection to the Box API. + * + *

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.

+ */ +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. + * @return an access token that can be used to authenticate an API request. + */ + @Override + public String getAccessToken() { return this.accessToken; } + + @Override + public void refresh() { + for(BoxAPIConnectionListener listener: listeners) + if (!listener.preRefresh(this)) + return; + + super.refresh(); + } + + @Override + public boolean needsRefresh() { return false; } +}