From 52f6923e40eb39e6885b5fadb24be639402a6461 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Mon, 15 Sep 2014 18:57:34 -0700 Subject: [PATCH 01/39] Remove Jetty logging from test output --- build.gradle | 2 ++ src/test/resources/log4j.properties | 1 + 2 files changed, 3 insertions(+) create mode 100644 src/test/resources/log4j.properties diff --git a/build.gradle b/build.gradle index 5e4293848..f659d1750 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,8 @@ dependencies { testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'com.github.tomakehurst:wiremock:1.47' testCompile 'org.mockito:mockito-core:1.9.5' + testCompile 'org.slf4j:slf4j-api:1.7.7' + testCompile 'org.slf4j:slf4j-nop:1.7.7' } tasks.withType(JavaCompile) { diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 000000000..8520d48c3 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1 @@ +log4j.rootLogger=OFF From a06b42f0b1782b2a2b9fc3e274173a1bc2395542 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 14:26:59 -0700 Subject: [PATCH 02/39] Improve request/response log formatting * Fix newlines. * Omit empty headers. * Make the first line of each record say if it's for a request or a response. * Indicate when a multipart request's file contents have been omitted from the log. --- src/main/java/com/box/sdk/BoxAPIRequest.java | 19 +++++++++++++--- src/main/java/com/box/sdk/BoxAPIResponse.java | 22 +++++++++++++++---- .../java/com/box/sdk/BoxMultipartRequest.java | 15 ++++++++++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxAPIRequest.java b/src/main/java/com/box/sdk/BoxAPIRequest.java index 1a1177fd8..c833edec8 100644 --- a/src/main/java/com/box/sdk/BoxAPIRequest.java +++ b/src/main/java/com/box/sdk/BoxAPIRequest.java @@ -100,30 +100,43 @@ public BoxAPIResponse send() { @Override public String toString() { StringBuilder builder = new StringBuilder(); + builder.append("Request"); + builder.append(System.lineSeparator()); builder.append(this.method); builder.append(' '); builder.append(this.url.toString()); builder.append(System.lineSeparator()); for (Map.Entry> entry : this.requestProperties.entrySet()) { + List nonEmptyValues = new ArrayList(); + for (String value : entry.getValue()) { + if (value != null && value.trim().length() != 0) { + nonEmptyValues.add(value); + } + } + + if (nonEmptyValues.size() == 0) { + continue; + } + builder.append(entry.getKey()); builder.append(": "); - for (String value : entry.getValue()) { + for (String value : nonEmptyValues) { builder.append(value); builder.append(", "); } builder.delete(builder.length() - 2, builder.length()); + builder.append(System.lineSeparator()); } String bodyString = this.bodyToString(); if (bodyString != null) { - builder.append(System.lineSeparator()); builder.append(System.lineSeparator()); builder.append(bodyString); } - return builder.toString(); + return builder.toString().trim(); } void setBackoffCounter(BackoffCounter counter) { diff --git a/src/main/java/com/box/sdk/BoxAPIResponse.java b/src/main/java/com/box/sdk/BoxAPIResponse.java index 74abef651..f21dc4882 100644 --- a/src/main/java/com/box/sdk/BoxAPIResponse.java +++ b/src/main/java/com/box/sdk/BoxAPIResponse.java @@ -5,6 +5,7 @@ import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -73,6 +74,8 @@ public void disconnect() { public String toString() { Map> headers = this.connection.getHeaderFields(); StringBuilder builder = new StringBuilder(); + builder.append("Response"); + builder.append(System.lineSeparator()); builder.append(this.connection.getRequestMethod()); builder.append(' '); builder.append(this.connection.getURL().toString()); @@ -86,24 +89,35 @@ public String toString() { continue; } + List nonEmptyValues = new ArrayList(); + for (String value : entry.getValue()) { + if (value != null && value.trim().length() != 0) { + nonEmptyValues.add(value); + } + } + + if (nonEmptyValues.size() == 0) { + continue; + } + builder.append(key); builder.append(": "); - for (String value : entry.getValue()) { + for (String value : nonEmptyValues) { builder.append(value); builder.append(", "); } builder.delete(builder.length() - 2, builder.length()); + builder.append(System.lineSeparator()); } String bodyString = this.bodyToString(); - if (bodyString != null) { - builder.append(System.lineSeparator()); + if (bodyString != null && bodyString != "") { builder.append(System.lineSeparator()); builder.append(bodyString); } - return builder.toString(); + return builder.toString().trim(); } protected String bodyToString() { diff --git a/src/main/java/com/box/sdk/BoxMultipartRequest.java b/src/main/java/com/box/sdk/BoxMultipartRequest.java index e9646739b..fae186c6e 100644 --- a/src/main/java/com/box/sdk/BoxMultipartRequest.java +++ b/src/main/java/com/box/sdk/BoxMultipartRequest.java @@ -17,15 +17,18 @@ public class BoxMultipartRequest extends BoxAPIRequest { private static final String BOUNDARY = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; private final StringBuilder loggedRequest = new StringBuilder(); + private OutputStream outputStream; private InputStream inputStream; private String filename; private Map fields; + private boolean firstBoundary; public BoxMultipartRequest(BoxAPIConnection api, URL url) { super(api, url, "POST"); this.fields = new HashMap(); + this.firstBoundary = true; this.addHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); } @@ -68,6 +71,10 @@ public void writeBody(HttpURLConnection connection) { b = this.inputStream.read(); } + if (LOGGER.isLoggable(Level.INFO)) { + this.loggedRequest.append(""); + } + for (Map.Entry entry : this.fields.entrySet()) { this.writePartHeader(new String[][] {{"name", entry.getKey()}}); this.writeOutput(entry.getValue()); @@ -81,6 +88,7 @@ public void writeBody(HttpURLConnection connection) { @Override protected void resetBody() throws IOException { + this.firstBoundary = true; this.inputStream.reset(); this.loggedRequest.setLength(0); } @@ -91,7 +99,12 @@ protected String bodyToString() { } private void writeBoundary() throws IOException { - this.writeOutput("\r\n--"); + if (!this.firstBoundary) { + this.writeOutput("\r\n"); + } + + this.firstBoundary = false; + this.writeOutput("--"); this.writeOutput(BOUNDARY); } From ffc33461091589b9d1cb6074ccfc8c713e337ee6 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 14:58:14 -0700 Subject: [PATCH 03/39] Improve stopping EventStream during a request EventStream will now stop by interrupting the polling thread. This has the benefit of also interrupting the HttpURLRequest instead of waiting for a timeout. --- src/main/java/com/box/sdk/EventStream.java | 36 +++++----- .../com/box/sdk/RealtimeServerConnection.java | 9 +-- .../java/com/box/sdk/EventStreamTest.java | 71 +++++++++++++++++-- 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/box/sdk/EventStream.java b/src/main/java/com/box/sdk/EventStream.java index 9776a359e..0c2af65f9 100644 --- a/src/main/java/com/box/sdk/EventStream.java +++ b/src/main/java/com/box/sdk/EventStream.java @@ -20,6 +20,7 @@ public class EventStream { private boolean started; private Poller poller; + private Thread pollerThread; public EventStream(BoxAPIConnection api) { this.api = api; @@ -34,13 +35,17 @@ public void addListener(EventListener listener) { } } + public boolean isStarted() { + return this.started; + } + public void stop() { if (!this.started) { throw new IllegalStateException("Cannot stop the EventStream because it isn't started."); } - this.poller.stop(); this.started = false; + this.pollerThread.interrupt(); } public void start() { @@ -54,13 +59,13 @@ public void start() { final long initialPosition = jsonObject.get("next_stream_position").asLong(); this.poller = new Poller(initialPosition); - Thread pollerThread = new Thread(this.poller); - pollerThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + this.pollerThread = new Thread(this.poller); + this.pollerThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { EventStream.this.notifyException(e); } }); - pollerThread.start(); + this.pollerThread.start(); this.started = true; } @@ -81,6 +86,10 @@ private void notifyEvent(BoxEvent event) { } private void notifyException(Throwable e) { + if (e instanceof InterruptedException && !this.started) { + return; + } + this.stop(); synchronized (this.listenerLock) { for (EventListener listener : this.listeners) { @@ -93,30 +102,25 @@ private void notifyException(Throwable e) { private class Poller implements Runnable { private final long initialPosition; - private final Object setServerLock; private RealtimeServerConnection server; - private boolean stopped; public Poller(long initialPosition) { this.initialPosition = initialPosition; - this.setServerLock = new Object(); this.server = new RealtimeServerConnection(EventStream.this.api); } @Override public void run() { long position = this.initialPosition; - while (!this.stopped) { + while (!Thread.interrupted()) { if (this.server.getRemainingRetries() == 0) { - synchronized (this.setServerLock) { - this.server = new RealtimeServerConnection(EventStream.this.api); - } + this.server = new RealtimeServerConnection(EventStream.this.api); } if (this.server.waitForChange(position)) { - if (this.stopped) { - break; + if (Thread.interrupted()) { + return; } BoxAPIRequest request = new BoxAPIRequest(EventStream.this.api, @@ -132,11 +136,5 @@ public void run() { } } } - - public void stop() { - synchronized (this.setServerLock) { - this.server.close(); - } - } } } diff --git a/src/main/java/com/box/sdk/RealtimeServerConnection.java b/src/main/java/com/box/sdk/RealtimeServerConnection.java index 2aacb8777..82e461ef0 100644 --- a/src/main/java/com/box/sdk/RealtimeServerConnection.java +++ b/src/main/java/com/box/sdk/RealtimeServerConnection.java @@ -33,12 +33,6 @@ int getRemainingRetries() { return this.retries; } - void close() { - if (this.response != null) { - this.response.disconnect(); - } - } - boolean waitForChange(long position) { if (this.retries < 1) { throw new IllegalStateException("No more retries are allowed."); @@ -46,7 +40,8 @@ boolean waitForChange(long position) { URL url; try { - url = new URL(this.serverURLString + "&stream_position=" + position); + String u = this.serverURLString + "&stream_position=" + position; + url = new URL(u); } catch (MalformedURLException e) { throw new BoxAPIException("The long poll URL was malformed.", e); } diff --git a/src/test/java/com/box/sdk/EventStreamTest.java b/src/test/java/com/box/sdk/EventStreamTest.java index a001e17e0..7bebd06b2 100644 --- a/src/test/java/com/box/sdk/EventStreamTest.java +++ b/src/test/java/com/box/sdk/EventStreamTest.java @@ -3,13 +3,25 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +import com.github.tomakehurst.wiremock.http.Request; +import com.github.tomakehurst.wiremock.http.RequestListener; +import com.github.tomakehurst.wiremock.http.Response; +import com.github.tomakehurst.wiremock.junit.WireMockRule; + public class EventStreamTest { + @Rule + public final WireMockRule wireMockRule = new WireMockRule(8080); + @Test @Category(IntegrationTest.class) public void receiveEventsForFolderCreateAndFolderDelete() throws InterruptedException { @@ -43,21 +55,68 @@ public boolean onException(Throwable e) { if (sourceFolder.getID().equals(expectedID)) { if (event.getType() == BoxEvent.Type.ITEM_CREATE) { BoxFolder folder = (BoxFolder) event.getSource(); - assertEquals(folder.getID(), childFolder.getID()); + final String eventFolderID = folder.getID(); + final String childFolderID = childFolder.getID(); + assertThat(eventFolderID, is(equalTo(childFolderID))); createdEventFound = true; } if (event.getType() == BoxEvent.Type.ITEM_TRASH) { BoxFolder folder = (BoxFolder) event.getSource(); - assertEquals(folder.getID(), childFolder.getID()); + assertThat(folder.getID(), is(equalTo(childFolder.getID()))); deletedEventFound = true; } } } } - assertTrue(createdEventFound); - assertTrue(deletedEventFound); + assertThat(createdEventFound, is(true)); + assertThat(deletedEventFound, is(true)); stream.stop(); } + + @Test + @Category(UnitTest.class) + public void canStopStreamWhileWaitingForAPIResponse() throws InterruptedException { + final long streamPosition = 0; + final String realtimeServerURL = "/realtimeServer?channel=0"; + + stubFor(options(urlEqualTo("/events")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{ \"entries\": [ { \"url\": \"http://localhost:8080" + realtimeServerURL + "\", " + + "\"max_retries\": \"3\", \"retry_timeout\": 60000 } ] }"))); + + stubFor(get(urlMatching("/events\\?.*stream_position=now.*")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{ \"next_stream_position\": " + streamPosition + " }"))); + + BoxAPIConnection api = new BoxAPIConnection(""); + api.setBaseURL("http://localhost:8080/"); + + final EventStream stream = new EventStream(api); + final Object requestLock = new Object(); + this.wireMockRule.addMockServiceRequestListener(new RequestListener() { + @Override + public void requestReceived(Request request, Response response) { + String streamPositionURL = realtimeServerURL + "&stream_position=" + streamPosition; + boolean requestUrlMatch = request.getUrl().contains(streamPositionURL); + if (requestUrlMatch) { + stream.stop(); + + synchronized (requestLock) { + requestLock.notify(); + } + } + } + }); + + stream.start(); + synchronized (requestLock) { + requestLock.wait(); + } + + assertThat(stream.isStarted(), is(false)); + } } From 48120ba1415626781eadfc263bd4c89750091318 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 15:48:51 -0700 Subject: [PATCH 04/39] Add parent field for BoxItems --- src/main/java/com/box/sdk/BoxFolder.java | 4 ++++ src/main/java/com/box/sdk/BoxItem.java | 11 +++++++++++ src/test/java/com/box/sdk/BoxFolderTest.java | 7 +++++++ 3 files changed, 22 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index 4e5d4f732..129003aff 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -92,6 +92,10 @@ public Info(String json) { super(json); } + protected Info(JsonObject jsonObject) { + super(jsonObject); + } + @Override public BoxFolder getResource() { return BoxFolder.this; diff --git a/src/main/java/com/box/sdk/BoxItem.java b/src/main/java/com/box/sdk/BoxItem.java index 7750fbcbc..83f3aae14 100644 --- a/src/main/java/com/box/sdk/BoxItem.java +++ b/src/main/java/com/box/sdk/BoxItem.java @@ -31,6 +31,7 @@ public abstract class Info extends BoxResource.Info { private Date contentModifiedAt; private BoxUser.Info ownedBy; private List tags; + private BoxFolder.Info parent; public Info() { super(); @@ -118,6 +119,10 @@ public List getTags() { return this.tags; } + public BoxFolder.Info getParent() { + return this.parent; + } + @Override protected void parseJSONMember(JsonObject.Member member) { super.parseJSONMember(member); @@ -173,6 +178,12 @@ protected void parseJSONMember(JsonObject.Member member) { case "tags": this.tags = this.parseTags(value.asArray()); break; + case "parent": + JsonObject jsonObject = value.asObject(); + String id = jsonObject.get("id").asString(); + BoxFolder parentFolder = new BoxFolder(getAPI(), id); + this.parent = parentFolder.new Info(jsonObject); + break; default: break; } diff --git a/src/test/java/com/box/sdk/BoxFolderTest.java b/src/test/java/com/box/sdk/BoxFolderTest.java index 28c992fdd..7fd8ccae3 100644 --- a/src/test/java/com/box/sdk/BoxFolderTest.java +++ b/src/test/java/com/box/sdk/BoxFolderTest.java @@ -46,15 +46,22 @@ public void getFolderInfoReturnsCorrectInfo() { final String expectedCreatedByID = currentUser.getID(); BoxFolder rootFolder = BoxFolder.getRootFolder(api); + final String expectedParentFolderID = rootFolder.getID(); + final String expectedParentFolderName = rootFolder.getInfo().getName(); + BoxFolder childFolder = rootFolder.createFolder(expectedName); BoxFolder.Info info = childFolder.getInfo(); String actualName = info.getName(); String actualCreatedByID = info.getCreatedBy().getID(); + String actualParentFolderID = info.getParent().getID(); + String actualParentFolderName = info.getParent().getName(); List actualPathCollection = info.getPathCollection(); assertThat(expectedName, equalTo(actualName)); assertThat(expectedCreatedByID, equalTo(actualCreatedByID)); + assertThat(expectedParentFolderID, equalTo(actualParentFolderID)); + assertThat(expectedParentFolderName, equalTo(actualParentFolderName)); assertThat(actualPathCollection, hasItem(rootFolder)); childFolder.delete(false); From b31b227521fa7031a76d728c395149eb3eef0bc0 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 16:30:20 -0700 Subject: [PATCH 05/39] Add folder copying --- src/main/java/com/box/sdk/BoxFolder.java | 33 ++++++++++++++++++++ src/test/java/com/box/sdk/BoxFolderTest.java | 23 ++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index 129003aff..b84c5ed47 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -11,6 +11,7 @@ public final class BoxFolder extends BoxItem implements Iterable { private static final String UPLOAD_FILE_URL_BASE = "https://upload.box.com/api/2.0/"; private static final URLTemplate CREATE_FOLDER_URL = new URLTemplate("folders"); + private static final URLTemplate COPY_FOLDER_URL = new URLTemplate("folders/%s/copy"); private static final URLTemplate DELETE_FOLDER_URL = new URLTemplate("folders/%s?recursive=%b"); private static final URLTemplate FOLDER_INFO_URL_TEMPLATE = new URLTemplate("folders/%s"); private static final URLTemplate UPLOAD_FILE_URL = new URLTemplate("files/content"); @@ -41,6 +42,38 @@ public void updateInfo(BoxFolder.Info info) { info.updateFromJSON(jsonObject); } + public BoxFolder.Info copy(BoxFolder destination) { + return this.copy(destination, null); + } + + public BoxFolder.Info copy(BoxFolder destination, String newName) { + return this.copy(destination.getID(), newName); + } + + public BoxFolder.Info copy(String destinationID) { + return this.copy(destinationID, null); + } + + public BoxFolder.Info copy(String destinationID, String newName) { + URL url = COPY_FOLDER_URL.build(this.getAPI().getBaseURL(), this.getID()); + BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST"); + + JsonObject parent = new JsonObject(); + parent.add("id", destinationID); + + JsonObject copyInfo = new JsonObject(); + copyInfo.add("parent", parent); + if (newName != null) { + copyInfo.add("name", newName); + } + + request.setBody(copyInfo.toString()); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); + BoxFolder copiedFolder = new BoxFolder(this.getAPI(), responseJSON.get("id").asString()); + return copiedFolder.new Info(responseJSON); + } + public BoxFolder createFolder(String name) { JsonObject parent = new JsonObject(); parent.add("id", this.getID()); diff --git a/src/test/java/com/box/sdk/BoxFolderTest.java b/src/test/java/com/box/sdk/BoxFolderTest.java index 7fd8ccae3..0ac7b96a4 100644 --- a/src/test/java/com/box/sdk/BoxFolderTest.java +++ b/src/test/java/com/box/sdk/BoxFolderTest.java @@ -7,6 +7,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; @@ -101,4 +102,26 @@ public void updateFolderInfoSucceeds() { childFolder.delete(false); assertThat(rootFolder, not(hasItem(childFolder))); } + + @Test + @Category(IntegrationTest.class) + public void copyFolderToSameDestinationWithNewNameSucceeds() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + final String originalName = "[copyFolderToSameDestinationWithNewNameSucceeds] Child Folder"; + final String newName = "[copyFolderToSameDestinationWithNewNameSucceeds] New Child Folder"; + + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder originalFolder = rootFolder.createFolder(originalName); + BoxFolder.Info copiedFolderInfo = originalFolder.copy(rootFolder, newName); + BoxFolder copiedFolder = copiedFolderInfo.getResource(); + + assertThat(copiedFolderInfo.getName(), is(equalTo(newName))); + assertThat(rootFolder, hasItem(originalFolder)); + assertThat(rootFolder, hasItem(copiedFolder)); + + originalFolder.delete(false); + copiedFolder.delete(false); + assertThat(rootFolder, not(hasItem(originalFolder))); + assertThat(rootFolder, not(hasItem(copiedFolder))); + } } From 720d90099e96a21ee2f2bf664446ade29a5aeaf5 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 17:27:15 -0700 Subject: [PATCH 06/39] Add folder moving --- src/main/java/com/box/sdk/BoxFolder.java | 18 +++++++++++++++ src/test/java/com/box/sdk/BoxFolderTest.java | 23 ++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index b84c5ed47..bfd791de4 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -98,6 +98,24 @@ public void delete(boolean recursive) { response.disconnect(); } + public void move(BoxFolder destination) { + this.move(destination.getID()); + } + + public void move(String destinationID) { + BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), this.folderURL, "PUT"); + + JsonObject parent = new JsonObject(); + parent.add("id", destinationID); + + JsonObject updateInfo = new JsonObject(); + updateInfo.add("parent", parent); + + request.setBody(updateInfo.toString()); + BoxAPIResponse response = request.send(); + response.disconnect(); + } + public BoxFile uploadFile(InputStream fileContent, String name, Date created, Date modified) { URL uploadURL = UPLOAD_FILE_URL.build(UPLOAD_FILE_URL_BASE); BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL); diff --git a/src/test/java/com/box/sdk/BoxFolderTest.java b/src/test/java/com/box/sdk/BoxFolderTest.java index 0ac7b96a4..b783be89d 100644 --- a/src/test/java/com/box/sdk/BoxFolderTest.java +++ b/src/test/java/com/box/sdk/BoxFolderTest.java @@ -124,4 +124,27 @@ public void copyFolderToSameDestinationWithNewNameSucceeds() { assertThat(rootFolder, not(hasItem(originalFolder))); assertThat(rootFolder, not(hasItem(copiedFolder))); } + + @Test + @Category(IntegrationTest.class) + public void moveFolderSucceeds() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + final String child1Name = "[moveFolderSucceeds] Child Folder"; + final String child2Name = "[moveFolderSucceeds] Child Folder 2"; + + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder childFolder1 = rootFolder.createFolder(child1Name); + BoxFolder childFolder2 = rootFolder.createFolder(child2Name); + + assertThat(rootFolder, hasItem(childFolder1)); + assertThat(rootFolder, hasItem(childFolder2)); + + childFolder2.move(childFolder1); + + assertThat(childFolder1, hasItem(childFolder2)); + assertThat(rootFolder, not(hasItem(childFolder2))); + + childFolder1.delete(true); + assertThat(rootFolder, not(hasItem(childFolder1))); + } } From e43b37d667ea2037534800d19e62b1160a63ef4c Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 17:34:40 -0700 Subject: [PATCH 07/39] Add folder renaming --- src/main/java/com/box/sdk/BoxFolder.java | 11 +++++++++++ src/test/java/com/box/sdk/BoxFolderTest.java | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index bfd791de4..64b59494a 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -116,6 +116,17 @@ public void move(String destinationID) { response.disconnect(); } + public void rename(String newName) { + BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), this.folderURL, "PUT"); + + JsonObject updateInfo = new JsonObject(); + updateInfo.add("name", newName); + + request.setBody(updateInfo.toString()); + BoxAPIResponse response = request.send(); + response.disconnect(); + } + public BoxFile uploadFile(InputStream fileContent, String name, Date created, Date modified) { URL uploadURL = UPLOAD_FILE_URL.build(UPLOAD_FILE_URL_BASE); BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL); diff --git a/src/test/java/com/box/sdk/BoxFolderTest.java b/src/test/java/com/box/sdk/BoxFolderTest.java index b783be89d..0f411fd7d 100644 --- a/src/test/java/com/box/sdk/BoxFolderTest.java +++ b/src/test/java/com/box/sdk/BoxFolderTest.java @@ -147,4 +147,22 @@ public void moveFolderSucceeds() { childFolder1.delete(true); assertThat(rootFolder, not(hasItem(childFolder1))); } + + @Test + @Category(IntegrationTest.class) + public void renameFolderSucceeds() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + final String originalName = "[renameFolderSucceeds] Original Name"; + final String newName = "[renameFolderSucceeds] New Name"; + + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder childFolder = rootFolder.createFolder(originalName); + childFolder.rename(newName); + + BoxFolder.Info childFolderInfo = childFolder.getInfo(); + assertThat(childFolderInfo.getName(), is(equalTo(newName))); + + childFolder.delete(false); + assertThat(rootFolder, not(hasItem(childFolder))); + } } From c3a96cef52568986b6ea9739bcf23d9b5049194e Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 18:14:19 -0700 Subject: [PATCH 08/39] Add fields support when getting folder info --- src/main/java/com/box/sdk/BoxFolder.java | 15 +++++++++++++ src/main/java/com/box/sdk/URLTemplate.java | 23 ++++++++++++++++++++ src/test/java/com/box/sdk/BoxFolderTest.java | 18 +++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index 64b59494a..a66a9c01a 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -34,6 +34,21 @@ public BoxFolder.Info getInfo() { return new Info(response.getJSON()); } + public BoxFolder.Info getInfo(String... fields) { + StringBuilder fieldsStringBuilder = new StringBuilder(); + for (String field : fields) { + fieldsStringBuilder.append(field); + fieldsStringBuilder.append(","); + } + fieldsStringBuilder.deleteCharAt(fieldsStringBuilder.length() - 1); + + String[][] queryParams = new String[][] {{"fields", fieldsStringBuilder.toString()}}; + URL url = FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), queryParams, this.getID()); + BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + return new Info(response.getJSON()); + } + public void updateInfo(BoxFolder.Info info) { BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), this.folderURL, "PUT"); request.setBody(info.toString()); diff --git a/src/main/java/com/box/sdk/URLTemplate.java b/src/main/java/com/box/sdk/URLTemplate.java index 19ab33f72..58caa7254 100644 --- a/src/main/java/com/box/sdk/URLTemplate.java +++ b/src/main/java/com/box/sdk/URLTemplate.java @@ -22,4 +22,27 @@ URL build(String base, Object... values) { return url; } + + URL build(String base, String[][] queryParams, Object... values) { + String baseURLString = String.format(base + this.template, values); + StringBuilder urlStringBuilder = new StringBuilder(baseURLString); + urlStringBuilder.append("?"); + for (String[] param : queryParams) { + urlStringBuilder.append(param[0]); + urlStringBuilder.append('='); + urlStringBuilder.append(param[1]); + urlStringBuilder.append('&'); + } + urlStringBuilder.deleteCharAt(urlStringBuilder.length() - 1); + + String urlString = urlStringBuilder.toString(); + URL url = null; + try { + url = new URL(urlString); + } catch (MalformedURLException e) { + assert false : "An invalid URL template indicates a bug in the SDK."; + } + + return url; + } } diff --git a/src/test/java/com/box/sdk/BoxFolderTest.java b/src/test/java/com/box/sdk/BoxFolderTest.java index 0f411fd7d..143a4db23 100644 --- a/src/test/java/com/box/sdk/BoxFolderTest.java +++ b/src/test/java/com/box/sdk/BoxFolderTest.java @@ -9,6 +9,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import org.junit.Test; @@ -69,6 +70,23 @@ public void getFolderInfoReturnsCorrectInfo() { assertThat(rootFolder, not(hasItem(childFolder))); } + @Test + @Category(IntegrationTest.class) + public void getInfoWithOnlyTheNameField() { + final String expectedName = "All Files"; + + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder.Info rootFolderInfo = rootFolder.getInfo("name"); + final String actualName = rootFolderInfo.getName(); + final String actualDescription = rootFolderInfo.getDescription(); + final long actualSize = rootFolderInfo.getSize(); + + assertThat(expectedName, equalTo(actualName)); + assertThat(actualDescription, is(nullValue())); + assertThat(actualSize, is(0L)); + } + @Test @Category(IntegrationTest.class) public void uploadFileSucceeds() { From 210c5e59987d968c269c5f131c14ad500f2df21d Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 16 Sep 2014 18:29:40 -0700 Subject: [PATCH 09/39] Remove package qualifiers from Javadocs --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index f659d1750..41d84384c 100644 --- a/build.gradle +++ b/build.gradle @@ -62,3 +62,7 @@ task integrationTest(type: Test) { includeCategories 'com.box.sdk.IntegrationTest' } } + +javadoc { + options.noQualifiers 'all' +} From 3f8f75e0b7eefc45be6b13c0481ab9cbea81ed7a Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Wed, 17 Sep 2014 23:15:31 -0700 Subject: [PATCH 10/39] Custom style for javadoc This stylesheet is a modified version of the default stylesheet used by the javadoc tool, therefore it's extremely ugly. In the future, it may be worth investigating writing a custom doclet instead of dealing with the horribly antiquated HTML generated by javadoc. --- build.gradle | 11 + doc/css/javadoc.css | 483 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 494 insertions(+) create mode 100644 doc/css/javadoc.css diff --git a/build.gradle b/build.gradle index 41d84384c..601d2712d 100644 --- a/build.gradle +++ b/build.gradle @@ -64,5 +64,16 @@ task integrationTest(type: Test) { } javadoc { + options.windowTitle 'Box Java SDK' options.noQualifiers 'all' + options.stylesheetFile file('doc/css/javadoc.css') + options.noTree true + options.noIndex true + options.noHelp true + options.noDeprecatedList true + options.noNavBar true + options.docEncoding 'utf-8' + options.charSet 'utf-8' + options.linkSource true + options.links 'http://docs.oracle.com/javase/8/docs/api/' } diff --git a/doc/css/javadoc.css b/doc/css/javadoc.css new file mode 100644 index 000000000..55f07a07a --- /dev/null +++ b/doc/css/javadoc.css @@ -0,0 +1,483 @@ +body { + color: #333; + font-family: 'Helvetica Neue', Calibri, sans-serif; + margin: 0; +} +hr { + display: none; +} +caption { + display: none; +} +a:link, +a:visited { + text-decoration: none; + color: rgb(26, 116, 186); +} +a:hover, +a:focus { + text-decoration: none; + color: rgb(22, 100, 160); +} +a:active { + text-decoration: none; + color: #4c6b87; +} +a[name] { + color: #353833; +} +a[name]:hover { + text-decoration: none; + color: #353833; +} +pre { + font-family: Menlo, Consolas, monospace; + font-size: 1em; +} +code { + font-family: Menlo, Consolas, monospace; + font-size: 1em; +} +h1 { + font-size: 2em; +} +h2 { + font-size: 1.7em; +} +h3 { + font-size: 1.5em; +} +h4 { + font-size: 1.2em; +} +h5 { + font-size: 1.1em; +} +h6 { + font-size: 1.1em; +} +ul { + list-style-type: disc; +} +table tr td dt code { + vertical-align: top; +} +sup { + font-size: .6em; +} +h3 a:link, +h3 a:visited { + text-decoration: none; + color: rgb(153, 153, 153); +} +h3 a:hover, +h3 a:focus { + text-decoration: none; + color: rgb(169, 169, 169); +} +tbody:first-of-type tr:nth-child(odd) { + background: #f3f3f3 +} +tbody:nth-of-type(2) tr:nth-child(even) { + background: #f3f3f3 +} +tbody:first-of-type tr:first-child { + font-size: 1.2em +} +.constantValuesContainer h2 { + margin: 0 0 0 .5882em; +} +.details { + margin: 1em; +} +.clear { + clear: both; + height: 0px; + overflow: hidden; +} +.aboutLanguage { + float: right; + padding: 0px 21px; + font-size: .8em; + z-index: 200; + margin-top: -7px; +} +.legalCopy { + margin-left: .5em; +} +.bar a, +.bar a:link, +.bar a:visited, +.bar a:active { + color: #FFFFFF; + text-decoration: none; +} +.bar a:hover, +.bar a:focus { + color: #bb7a2a; +} +.tab { + background-color: #0066FF; + background-image: url(resources/titlebar.gif); + background-position: left top; + background-repeat: no-repeat; + color: #ffffff; + padding: 8px; + width: 5em; + font-weight: bold; +} +.bar { + display: none; +} +ul.navList, +ul.subNavList { + float: left; + margin: 0 25px 0 0; + padding: 0; +} +ul.navList li { + list-style: none; + float: left; + padding: 3px 6px; +} +ul.subNavList li { + list-style: none; + float: left; + font-size: 90%; +} +.topNav a:link, +.topNav a:active, +.topNav a:visited, +.bottomNav a:link, +.bottomNav a:active, +.bottomNav a:visited { + color: rgb(153, 153, 153); + text-decoration: none; +} +.topNav a:hover, +.bottomNav a:hover { + text-decoration: none; + color: #fff; +} +.navBarCell1Rev { + font-weight: bold; +} +.header, +.footer { + clear: both; + margin: 1em; +} +.indexHeader { + margin: 10px; + position: relative; +} +.indexHeader h1 { + font-size: 1.3em; +} +.subTitle { + display: none; +} +.header ul { + margin: 0 0 25px 0; + padding: 0; +} +.footer ul { + margin: 20px 0 5px 0; +} +.header ul li, +.footer ul li { + list-style: none; + font-size: 1.2em; +} +ul.blockList ul.blockList ul.blockList li.blockList h3 { + background-color: rgb(64, 64, 64); + color: #fff; + font-size: 1.2em; + padding: .5em 1em .5em 1em; +} +ul.blockList ul.blockList li.blockList h3 { + display: none; +} +ul.blockList li.blockList h2 { + padding: 0px 0 20px 0; +} +div.summary ul.blockList li.blockList ul.blockList li.blockList ul.blockList code { + display: block; + margin: 1em; +} +div.summary ul.blockList li.blockList ul.blockList li.blockList ul.blockList li.blockList h3 { + display: block; +} +.contentContainer, +.sourceContainer, +.classUseContainer, +.serializedFormContainer, +.constantValuesContainer { + clear: both; + position: relative; +} +.indexContainer { + background-color: rgb(64, 64, 64); + margin: 10px; + position: relative; + font-size: 1.0em; +} +.indexContainer h2 { + font-size: 1.1em; + padding: 0 0 3px 0; +} +.indexContainer ul { + margin: 0; + padding: 0; +} +.indexContainer ul li { + list-style: none; +} +.contentContainer .description dl dt, +.contentContainer .details dl dt, +.serializedFormContainer dl dt { + font-size: 1.1em; + font-weight: bold; + margin: 10px 0 0 0; + color: #4E4E4E; +} +.contentContainer .description dl dd, +.contentContainer .details dl dd, +.serializedFormContainer dl dd { + margin: 10px 0 10px 20px; +} +.serializedFormContainer dl.nameValue dt { + margin-left: 1px; + font-size: 1.1em; + display: inline; + font-weight: bold; +} +.serializedFormContainer dl.nameValue dd { + margin: 0 0 0 1px; + font-size: 1.1em; + display: inline; +} +ul.horizontal li { + display: inline; + font-size: 0.9em; +} +ul.inheritance { + margin-left: 1em; + padding: 0; +} +ul.inheritance li { + display: inline; + list-style: none; +} +ul.inheritance li ul.inheritance { + margin-left: 15px; + padding-left: 15px; + padding-top: 1px; +} +ul.blockList, +ul.blockListLast { + margin: 1em 0 1em 0; + padding: 0; +} +ul.blockList h4, +ul.blockListLast h4 { + background-color: rgb(64, 64, 64); + color: #fff; + margin: 0 -.8333em 0 -.8333em; + padding: .5em 1em .5em 1em; +} +ul.blockList li.blockList, +ul.blockListLast li.blockList { + list-style: none; + margin-bottom: 25px; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { + margin-left: 0; + padding-left: 0; + padding-bottom: 15px; + border: none; + border-bottom: 1px solid #9eadc0; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { + list-style: none; + border-bottom: none; + padding-bottom: 0; +} +table tr td dl, +table tr td dl dt, +table tr td dl dd { + margin-top: 0; + margin-bottom: 1px; +} +.contentContainer table, +.classUseContainer table, +.constantValuesContainer table { + width: 100%; +} +.contentContainer ul li table, +.classUseContainer ul li table, +.constantValuesContainer ul li table { + width: 100%; +} +.contentContainer .description table, +.contentContainer .details table { + border-bottom: none; +} +.contentContainer ul li table th.colOne, +.contentContainer ul li table th.colFirst, +.contentContainer ul li table th.colLast, +.classUseContainer ul li table th, +.constantValuesContainer ul li table th, +.contentContainer ul li table td.colOne, +.contentContainer ul li table td.colFirst, +.contentContainer ul li table td.colLast, +.classUseContainer ul li table td, +.constantValuesContainer ul li table td { + vertical-align: top; +} +.overviewSummary caption, +.packageSummary caption, +.contentContainer ul.blockList li.blockList caption, +.summary caption, +.classUseContainer caption, +.constantValuesContainer caption { + position: relative; + text-align: left; + background-repeat: no-repeat; + color: #FFFFFF; + font-weight: bold; + clear: none; + overflow: hidden; + padding: 0px; + margin: 0px; +} +caption a:link, +caption a:hover, +caption a:active, +caption a:visited { + color: #FFFFFF; +} +.overviewSummary caption span, +.packageSummary caption span, +.contentContainer ul.blockList li.blockList caption span, +.summary caption span, +.classUseContainer caption span, +.constantValuesContainer caption span { + white-space: nowrap; + padding-top: 8px; + padding-left: 8px; + display: block; + float: left; + background-image: url(resources/titlebar.gif); + height: 18px; +} +.overviewSummary .tabEnd, +.packageSummary .tabEnd, +.contentContainer ul.blockList li.blockList .tabEnd, +.summary .tabEnd, +.classUseContainer .tabEnd, +.constantValuesContainer .tabEnd { + width: 10px; + background-image: url(resources/titlebar_end.gif); + background-repeat: no-repeat; + background-position: top right; + position: relative; + float: left; +} +ul.blockList ul.blockList li.blockList table { + width: 100%; +} +.tableSubHeadingColor { + background-color: #EEEEFF; +} +.rowColor { + background-color: #ffffff; +} +.overviewSummary td, +.packageSummary td, +.contentContainer ul.blockList li.blockList td, +.summary td, +.classUseContainer td, +.constantValuesContainer td { + text-align: left; + padding: .5em 1em .5em 1em; +} +th.colFirst, +th.colLast, +th.colOne, +.constantValuesContainer th { + background: rgb(64, 64, 64); + color: #fff; + text-align: left; + padding: .5em 1em .5em 1em; +} +td.colOne a:link, +td.colOne a:active, +td.colOne a:visited, +td.colOne a:hover, +td.colFirst a:link, +td.colFirst a:active, +td.colFirst a:visited, +td.colFirst a:hover, +td.colLast a:link, +td.colLast a:active, +td.colLast a:visited, +td.colLast a:hover, +.constantValuesContainer td a:link, +.constantValuesContainer td a:active, +.constantValuesContainer td a:visited, +.constantValuesContainer td a:hover { + font-weight: bold; +} +td.colFirst, +th.colFirst { + white-space: nowrap; +} +table.overviewSummary { + padding: 0px; + margin-left: 0px; +} +table.overviewSummary td.colFirst, +table.overviewSummary th.colFirst, +table.overviewSummary td.colOne, +table.overviewSummary th.colOne { + width: 25%; + vertical-align: middle; +} +table.packageSummary td.colFirst, +table.overviewSummary th.colFirst { + width: 25%; + vertical-align: middle; +} +div.description { + margin: 1em; +} +.description pre { + margin-top: 0; +} +.description .block { + margin: 2em 0 2em 0; +} +.deprecatedContent { + margin: 0; + padding: 10px 0; +} +.docSummary { + padding: 0; +} +.sourceLineNo { + color: green; + padding: 0 30px 0 0; +} +h1.hidden { + visibility: hidden; + overflow: hidden; + font-size: .9em; +} +.block { + display: block; + margin: 3px 0 0 0; +} +.strong { + font-weight: bold; +} From 8115e3949a132b9ee1ccef63595d3c1fa9a22fcc Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Wed, 17 Sep 2014 23:23:55 -0700 Subject: [PATCH 11/39] Rename maxAttempts in BoxAPIConnection Also add some documentation for the getters and setters. --- .../java/com/box/sdk/BoxAPIConnection.java | 22 +++++++++++++------ src/main/java/com/box/sdk/BoxAPIRequest.java | 2 +- .../java/com/box/sdk/BoxAPIRequestTest.java | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index 57ccf390d..49bd9d60a 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -33,7 +33,7 @@ public class BoxAPIConnection { private String accessToken; private String refreshToken; private boolean autoRefresh; - private int maxAttempts; + private int maxRequestAttempts; /** * Constructs a new BoxAPIConnection that authenticates with a developer or access token. @@ -57,7 +57,7 @@ public BoxAPIConnection(String clientID, String clientSecret, String accessToken this.setRefreshToken(refreshToken); this.baseURL = DEFAULT_BASE_URL; this.autoRefresh = true; - this.maxAttempts = DEFAULT_MAX_ATTEMPTS; + this.maxRequestAttempts = DEFAULT_MAX_ATTEMPTS; } /** @@ -94,7 +94,7 @@ public BoxAPIConnection(String clientID, String clientSecret, String authCode) { } /** - * Set the amount of time for which this connection's access token is valid before it must be refreshed. + * Sets the amount of time for which this connection's access token is valid before it must be refreshed. * @param milliseconds the number of milliseconds for which the access token is valid. */ public void setExpires(long milliseconds) { @@ -181,12 +181,20 @@ public boolean getAutoRefresh() { return this.autoRefresh; } - public int getMaxAttempts() { - return this.maxAttempts; + /** + * Gets the maximum number of times an API request will be tried when an error occurs. + * @return the maximum number of request attempts. + */ + public int getMaxRequestAttempts() { + return this.maxRequestAttempts; } - public void setMaxAttempts(int attempts) { - this.maxAttempts = attempts; + /** + * Sets the maximum number of times an API request will be tried when an error occurs. + * @param attempts the maximum number of request attempts. + */ + public void setMaxRequestAttempts(int attempts) { + this.maxRequestAttempts = attempts; } /** diff --git a/src/main/java/com/box/sdk/BoxAPIRequest.java b/src/main/java/com/box/sdk/BoxAPIRequest.java index c833edec8..560519483 100644 --- a/src/main/java/com/box/sdk/BoxAPIRequest.java +++ b/src/main/java/com/box/sdk/BoxAPIRequest.java @@ -68,7 +68,7 @@ public BoxAPIResponse send() { if (this.api == null) { this.backoffCounter.reset(BoxAPIConnection.DEFAULT_MAX_ATTEMPTS); } else { - this.backoffCounter.reset(this.api.getMaxAttempts()); + this.backoffCounter.reset(this.api.getMaxRequestAttempts()); } while (this.backoffCounter.getAttemptsRemaining() > 0) { diff --git a/src/test/java/com/box/sdk/BoxAPIRequestTest.java b/src/test/java/com/box/sdk/BoxAPIRequestTest.java index 90a60ccb9..d3cf1c8d0 100644 --- a/src/test/java/com/box/sdk/BoxAPIRequestTest.java +++ b/src/test/java/com/box/sdk/BoxAPIRequestTest.java @@ -64,7 +64,7 @@ public void requestRetriesTheNumberOfTimesConfiguredInTheAPIConnection() throws BackoffCounter backoffCounter = new BackoffCounter(mockTime); BoxAPIConnection api = new BoxAPIConnection(""); - api.setMaxAttempts(expectedNumAttempts); + api.setMaxRequestAttempts(expectedNumAttempts); URL url = new URL("http://localhost:8080/"); BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); From 7b8101264a74159f6b87be3883dcc19738160bd5 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 00:48:27 -0700 Subject: [PATCH 12/39] Add documentation to BoxAPIRequest --- src/main/java/com/box/sdk/BoxAPIRequest.java | 102 ++++++++++++++++++- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxAPIRequest.java b/src/main/java/com/box/sdk/BoxAPIRequest.java index 560519483..fdc337178 100644 --- a/src/main/java/com/box/sdk/BoxAPIRequest.java +++ b/src/main/java/com/box/sdk/BoxAPIRequest.java @@ -14,6 +14,23 @@ import java.util.logging.Level; import java.util.logging.Logger; +/** + * Used to make HTTP requests to the Box API. + * + *

All communication with the REST API is done through this class or one of its subclasses. This class wraps {@link + * HttpURLConnection} in order to provide a simpler interface that can automatically handle various conditions specific + * to Box's API. Requests will be authenticated using a {@link BoxAPIConnection} (if one is provided), so it isn't + * necessary to add authorization headers. Requests can also be sent more than once, unlike with HttpURLConnection. If + * an error occurs while sending a request, it will be automatically retried (with a back off delay) up to the maximum + * number of times set in the BoxAPIConnection.

+ * + *

Specifying a body for a BoxAPIRequest is done differently than it is with HttpURLConnection. Instead of writing to + * an OutputStream, the request is provided an {@link InputStream} which will be read when the {@link #send} method is + * called. This makes it easy to retry requests since the stream can automatically reset and reread with each attempt. + * If the stream cannot be reset, then a new stream will need to be provided before each call to send. There is also a + * convenience method for specifying the body as a String, which simply wraps the String with an InputStream.

+ * + */ public class BoxAPIRequest { private static final Logger LOGGER = Logger.getLogger(BoxAPIRequest.class.getName()); @@ -28,10 +45,21 @@ public class BoxAPIRequest { private long bodyLength; private Map> requestProperties; + /** + * Constructs an unauthenticated BoxAPIRequest. + * @param url the URL of the request. + * @param method the HTTP method of the request. + */ public BoxAPIRequest(URL url, String method) { this(null, url, method); } + /** + * Constructs an authenticated BoxAPIRequest using a provided BoxAPIConnection. + * @param api an API connection for authenticating the request. + * @param url the URL of the request. + * @param method the HTTP method of the request. + */ public BoxAPIRequest(BoxAPIConnection api, URL url, String method) { this.api = api; this.url = url; @@ -43,27 +71,64 @@ public BoxAPIRequest(BoxAPIConnection api, URL url, String method) { this.addHeader("Accept-Charset", "utf-8"); } + /** + * Adds an HTTP header to this request. + * @param key the header key. + * @param value the header value. + */ public void addHeader(String key, String value) { this.headers.add(new RequestHeader(key, value)); } + /** + * Sets a timeout for this request in milliseconds. + * @param timeout the timeout in milliseconds. + */ public void setTimeout(int timeout) { this.timeout = timeout; } - public void setContentLength(long length) { - this.bodyLength = length; - } - + /** + * Sets the request body to the contents of an InputStream. + * + *

The stream must support the {@link InputStream#reset} method if auto-retry is used or if the request needs to + * be resent. Otherwise, the body must be manually set before each call to {@link #send}. + * + * @param stream an InputStream containing the contents of the body. + */ public void setBody(InputStream stream) { this.body = stream; } + /** + * Sets the request body to the contents of a String. + * + *

If the contents of the body are large, then it may be more efficient to use an {@link InputStream} instead of + * a String. Using a String requires that the entire body be in memory before sending the request.

+ * + * @param body a String containing the contents of the body. + */ public void setBody(String body) { this.bodyLength = body.length(); this.body = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); } + /** + * Sends this request and returns a BoxAPIResponse containing the server's response. + * + *

The type of the returned BoxAPIResponse will be based on the content type returned by the server, allowing it + * to be cast to a more specific type. For example, if it's known that the API call will return a JSON response, + * then it can be cast to a {@link BoxJSONResponse} like so:

+ * + *
BoxJSONResponse response = (BoxJSONResponse) request.send();
+ * + *

If the server returns an error code or if a network error occurs, then the request will be automatically + * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException} + * will be thrown.

+ * + * @throws BoxAPIException if the server returns an error code or if a network error occurs. + * @return a {@link BoxAPIResponse} containing the server's response. + */ public BoxAPIResponse send() { if (this.api == null) { this.backoffCounter.reset(BoxAPIConnection.DEFAULT_MAX_ATTEMPTS); @@ -97,6 +162,10 @@ public BoxAPIResponse send() { throw new RuntimeException(); } + /** + * Returns a String containing the URL, HTTP method, headers and body of this request. + * @return a String containing information about this request. + */ @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -143,10 +212,27 @@ void setBackoffCounter(BackoffCounter counter) { this.backoffCounter = counter; } + /** + * Returns a String representation of this request's body used in {@link #toString}. This method returns + * null by default. + * + *

A subclass may want override this method if the body can be converted to a String for logging or debugging + * purposes.

+ * + * @return a String representation of this request's body. + */ protected String bodyToString() { return null; } + /** + * Writes the body of this request to an HttpURLConnection. + * + *

Subclasses overriding this method must remember to close the connection's OutputStream after writing.

+ * + * @param connection the connection to which the body should be written. + * @throws BoxAPIException if an error occurs while writing to the connection. + */ protected void writeBody(HttpURLConnection connection) { if (this.body == null) { return; @@ -166,6 +252,14 @@ protected void writeBody(HttpURLConnection connection) { } } + /** + * Resets the InputStream containing this request's body. + * + *

This method will be called before each attempt to resend the request, giving subclasses an opportunity to + * reset any streams that need to be read when sending the body.

+ * + * @throws IOException if the stream cannot be reset. + */ protected void resetBody() throws IOException { if (this.body != null) { this.body.reset(); From f530ed2e49c3b8b87450ee412be88cff56e5407c Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 12:05:35 -0700 Subject: [PATCH 13/39] Add javadoc link to readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a912002d..abcc9c7fd 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,6 @@ You can find guides and tutorials in the `doc` directory. * [Authentication](doc/authentication.md) * [Events Stream](doc/events.md) -Javadocs are also generated when `gradle build` is run and can be found in `build/doc/javadoc`. +Javadoc reference documentation is [available here][1]. Javadocs are also generated when `gradle javadoc` is run and can be found in `build/doc/javadoc`. + +[1]:https://gitenterprise.inside-box.net/pages/Box/box-java-sdk/javadoc/com/box/sdk/package-summary.html From 8fbc6025659b5df050b2f3f29796a422de30bb93 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 12:08:48 -0700 Subject: [PATCH 14/39] Line wrap the readme --- README.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index abcc9c7fd..a1502e3ee 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ Box Java SDK ============ -This SDK provides a Java interface for the [Box REST API](https://developers.box.com/docs/). Features from the [previous version of the Box Java SDK](https://github.com/box/box-java-sdk-v2) are being transitioned to this SDK. +This SDK provides a Java interface for the [Box REST +API](https://developers.box.com/docs/). Features from the [previous version of +the Box Java SDK](https://github.com/box/box-java-sdk-v2) are being transitioned +to this SDK. Quickstart ---------- -Here is a simple example of how to authenticate with the API using a developer token and then print the ID and name of each item in your root folder. +Here is a simple example of how to authenticate with the API using a developer +token and then print the ID and name of each item in your root folder. ```java BoxAPIConnection api = new BoxAPIConnection("developer-token"); @@ -20,13 +24,19 @@ for (BoxItem item : rootFolder) { Building -------- -The SDK uses Gradle for its build system. Running `gradle build` from the root of the repository will compile, lint, and test the SDK. +The SDK uses Gradle for its build system. Running `gradle build` from the root +of the repository will compile, lint, and test the SDK. ```bash $ gradle build ``` -The SDK also includes integration tests which make real API calls, and therefore are run separately from unit tests. Integration tests should be run against a test account since they create and delete data. To run the integration tests, remove the `.template` extension from `src/test/config/config.properties.template` and fill in your test account's information. Then run: +The SDK also includes integration tests which make real API calls, and therefore +are run separately from unit tests. Integration tests should be run against a +test account since they create and delete data. To run the integration tests, +remove the `.template` extension from +`src/test/config/config.properties.template` and fill in your test account's +information. Then run: ```bash $ gradle integrationTest @@ -41,6 +51,7 @@ You can find guides and tutorials in the `doc` directory. * [Authentication](doc/authentication.md) * [Events Stream](doc/events.md) -Javadoc reference documentation is [available here][1]. Javadocs are also generated when `gradle javadoc` is run and can be found in `build/doc/javadoc`. +Javadoc reference documentation is [available here][1]. Javadocs are also +generated when `gradle javadoc` is run and can be found in `build/doc/javadoc`. [1]:https://gitenterprise.inside-box.net/pages/Box/box-java-sdk/javadoc/com/box/sdk/package-summary.html From a6a2437c5e9d77d2c039ecf1020137b185123449 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 13:03:28 -0700 Subject: [PATCH 15/39] Remove some overloaded copy methods from BoxFolder --- src/main/java/com/box/sdk/BoxFolder.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index a66a9c01a..b24d992e5 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -62,19 +62,11 @@ public BoxFolder.Info copy(BoxFolder destination) { } public BoxFolder.Info copy(BoxFolder destination, String newName) { - return this.copy(destination.getID(), newName); - } - - public BoxFolder.Info copy(String destinationID) { - return this.copy(destinationID, null); - } - - public BoxFolder.Info copy(String destinationID, String newName) { URL url = COPY_FOLDER_URL.build(this.getAPI().getBaseURL(), this.getID()); BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST"); JsonObject parent = new JsonObject(); - parent.add("id", destinationID); + parent.add("id", destination.getID()); JsonObject copyInfo = new JsonObject(); copyInfo.add("parent", parent); From e8e4c310ddc8e418a912af319d803f374a9b110d Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 13:12:48 -0700 Subject: [PATCH 16/39] Add documentation for EventListener --- src/main/java/com/box/sdk/EventListener.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/com/box/sdk/EventListener.java b/src/main/java/com/box/sdk/EventListener.java index edd862e93..aa065b40d 100644 --- a/src/main/java/com/box/sdk/EventListener.java +++ b/src/main/java/com/box/sdk/EventListener.java @@ -1,7 +1,23 @@ package com.box.sdk; +/** + * The listener interface for receiving events from an {@link EventStream}. + */ public interface EventListener { + /** + * Invoked when an event is received from the API. + * @param event the received event. + */ void onEvent(BoxEvent event); + /** + * Invoked when an error occurs while waiting for events to be received. + * + *

When an EventStream encounters an exception, it will invoke this method on each of its listeners until one + * of them returns true, indicating that the exception was handled.

+ * + * @param e the exception that was thrown while waiting for events. + * @return true if the exception was handled; otherwise false. + */ boolean onException(Throwable e); } From 7a20cb7fe1644e87a48a926c7fc689d13b25d38a Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 13:27:53 -0700 Subject: [PATCH 17/39] Add documentation for EventStream --- src/main/java/com/box/sdk/EventStream.java | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/com/box/sdk/EventStream.java b/src/main/java/com/box/sdk/EventStream.java index 0c2af65f9..e7a7f323e 100644 --- a/src/main/java/com/box/sdk/EventStream.java +++ b/src/main/java/com/box/sdk/EventStream.java @@ -8,6 +8,15 @@ import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; +/** + * Receives real-time events from the API and forwards them to {@link EventListener EventListeners}. + * + *

This class handles long polling the Box events endpoint in order to receive real-time user or enterprise events. + * When an EventStream is started, it begins long polling on a separate thread until the {@link #stop} method is called. + * Since the API may return duplicate events, EventStream also maintains a small cache of the most recently received + * event IDs in order to automatically deduplicate events.

+ * + */ public class EventStream { private static final int LIMIT = 800; private static final int LRU_SIZE = 512; @@ -22,6 +31,10 @@ public class EventStream { private Poller poller; private Thread pollerThread; + /** + * Constructs an EventStream using an API connection. + * @param api the API connection to use. + */ public EventStream(BoxAPIConnection api) { this.api = api; this.listeners = new ArrayList(); @@ -29,16 +42,28 @@ public EventStream(BoxAPIConnection api) { this.receivedEvents = new LinkedHashSet(LRU_SIZE); } + /** + * Adds a listener that will be notified when an event is received. + * @param listener the listener to add. + */ public void addListener(EventListener listener) { synchronized (this.listenerLock) { this.listeners.add(listener); } } + /** + * Indicates whether or not this EventStream has been started. + * @return true if this EventStream has been started; otherwise false. + */ public boolean isStarted() { return this.started; } + /** + * Stops this EventStream and disconnects from the API. + * @throws IllegalStateException if the EventStream is already stopped. + */ public void stop() { if (!this.started) { throw new IllegalStateException("Cannot stop the EventStream because it isn't started."); @@ -48,6 +73,10 @@ public void stop() { this.pollerThread.interrupt(); } + /** + * Starts this EventStream and begins long polling the API. + * @throws IllegalStateException if the EventStream is already started. + */ public void start() { if (this.started) { throw new IllegalStateException("Cannot start the EventStream because it isn't stopped."); From 1d1807eb4a93a5609e0727a48ca416015a503a17 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 16:12:01 -0700 Subject: [PATCH 18/39] Add documentation for BoxResource --- src/main/java/com/box/sdk/BoxFolder.java | 2 +- src/main/java/com/box/sdk/BoxResource.java | 95 ++++++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index b24d992e5..cad7566c5 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -51,7 +51,7 @@ public BoxFolder.Info getInfo(String... fields) { public void updateInfo(BoxFolder.Info info) { BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), this.folderURL, "PUT"); - request.setBody(info.toString()); + request.setBody(info.getPendingChanges()); BoxJSONResponse response = (BoxJSONResponse) request.send(); JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); info.updateFromJSON(jsonObject); diff --git a/src/main/java/com/box/sdk/BoxResource.java b/src/main/java/com/box/sdk/BoxResource.java index bfb59d24d..54a7b32e2 100644 --- a/src/main/java/com/box/sdk/BoxResource.java +++ b/src/main/java/com/box/sdk/BoxResource.java @@ -4,23 +4,49 @@ import com.eclipsesource.json.JsonObject; +/** + * The abstract base class for all resource types (files, folders, comments, collaborations, etc.) used by the API. + * + *

Every API resource has an ID and a {@link BoxAPIConnection} that it uses to communicate with the API. Some + * resources also have an associated {@link Info} class that can contain additional information about the resource.

+ * + */ public abstract class BoxResource { private final BoxAPIConnection api; private final String id; + /** + * Constructs a BoxResource for a resource with a given ID. + * @param api the API connection to be used by the resource. + * @param id the ID of the resource. + */ public BoxResource(BoxAPIConnection api, String id) { this.api = api; this.id = id; } + /** + * Gets the API connection used by this resource. + * @return the API connection used by this resource. + */ public BoxAPIConnection getAPI() { return this.api; } + /** + * Gets the ID of this resource. + * @return the ID of this resource. + */ public String getID() { return this.id; } + /** + * Indicates whether this BoxResource is equal to another BoxResource. Two BoxResources are equal if they have the + * same type and ID. + * @param other the other BoxResource to compare. + * @return true if the type and IDs of the two resources are equal; otherwise false. + */ @Override public boolean equals(Object other) { if (other == null) { @@ -35,49 +61,100 @@ public boolean equals(Object other) { return false; } + /** + * Returns a hash code value for this BoxResource. + * @return a hash code value for this BoxResource. + */ @Override public int hashCode() { return this.getID().hashCode(); } + /** + * Contains additional information about a BoxResource. + * + *

Subclasses should track any changes to a resource's information by calling the {@link #addPendingChange} + * method. The pending changes will then be serialized to JSON when {@link #getPendingChanges} is called.

+ * + * @param the type of the resource associated with this info. + */ public abstract class Info { private JsonObject pendingChanges; + /** + * Constructs an empty Info object. + */ public Info() { this.pendingChanges = new JsonObject(); } + /** + * Constructs an Info object by parsing information from a JSON string. + * @param json the JSON string to parse. + */ public Info(String json) { this(JsonObject.readFrom(json)); } + /** + * Constructs an Info object using an already parsed JSON object. + * @param jsonObject the parsed JSON object. + */ protected Info(JsonObject jsonObject) { this.updateFromJSON(jsonObject); } + /** + * Gets the ID of the resource associated with this Info. + * @return the ID of the associated resource. + */ public String getID() { return BoxResource.this.getID(); } - public List getPendingChanges() { + /** + * Gets a list of fields that have pending changes that haven't been sent to the API yet. + * @return a list of changed fields with pending changes. + */ + public List getChangedFields() { return this.pendingChanges.names(); } - public abstract T getResource(); - - @Override - public String toString() { + /** + * Gets a JSON object of any pending changes. + * @return a JSON object containing the pending changes. + */ + public String getPendingChanges() { return this.pendingChanges.toString(); } + /** + * Gets the resource associated with this Info. + * @return the associated resource. + */ + public abstract T getResource(); + + /** + * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next + * time {@link #getPendingChanges} is called. + * @param key the name of the field. + * @param value the new value of the field. + */ protected void addPendingChange(String key, String value) { this.pendingChanges.set(key, value); } + /** + * Clears all pending changes. + */ protected void clearPendingChanges() { this.pendingChanges = new JsonObject(); } + /** + * Updates this Info object using the information in a JSON object. + * @param jsonObject the JSON object containing updated information. + */ protected void updateFromJSON(JsonObject jsonObject) { for (JsonObject.Member member : jsonObject) { if (member.getValue().isNull()) { @@ -90,6 +167,14 @@ protected void updateFromJSON(JsonObject jsonObject) { this.clearPendingChanges(); } + /** + * Invoked with a JSON member whenever this Info object is updated or created from a JSON object. + * + *

Subclasses should override this method in order to parse any JSON members it knows about. This method is a + * no-op by default.

+ * + * @param member the JSON member to be parsed. + */ protected void parseJSONMember(JsonObject.Member member) { } } } From 253a1bc57ab499183579cf21a54650009b8d70aa Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 16:57:28 -0700 Subject: [PATCH 19/39] Make EventStream de-duping configurable Add a protected method to EventStream that determines if an event ID is a duplicate. This method can then be overridden by a subclass in order to perform custom de-duping logic. Closes #12. --- src/main/java/com/box/sdk/EventStream.java | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/box/sdk/EventStream.java b/src/main/java/com/box/sdk/EventStream.java index e7a7f323e..9cac54bcd 100644 --- a/src/main/java/com/box/sdk/EventStream.java +++ b/src/main/java/com/box/sdk/EventStream.java @@ -25,8 +25,8 @@ public class EventStream { private final BoxAPIConnection api; private final Collection listeners; private final Object listenerLock; - private final LinkedHashSet receivedEvents; + private LinkedHashSet receivedEvents; private boolean started; private Poller poller; private Thread pollerThread; @@ -39,7 +39,6 @@ public EventStream(BoxAPIConnection api) { this.api = api; this.listeners = new ArrayList(); this.listenerLock = new Object(); - this.receivedEvents = new LinkedHashSet(LRU_SIZE); } /** @@ -99,14 +98,23 @@ public void uncaughtException(Thread t, Throwable e) { this.started = true; } + protected boolean isDuplicate(String eventID) { + if (this.receivedEvents == null) { + this.receivedEvents = new LinkedHashSet(LRU_SIZE); + } + + boolean newEvent = this.receivedEvents.add(eventID); + if (newEvent && this.receivedEvents.size() > LRU_SIZE) { + this.receivedEvents.iterator().remove(); + } + + return newEvent; + } + private void notifyEvent(BoxEvent event) { synchronized (this.listenerLock) { - boolean newEvent = this.receivedEvents.add(event.getID()); - if (newEvent) { - if (this.receivedEvents.size() > LRU_SIZE) { - this.receivedEvents.iterator().remove(); - } - + boolean isDuplicate = this.isDuplicate(event.getID()); + if (!isDuplicate) { for (EventListener listener : this.listeners) { listener.onEvent(event); } From 72b8e9f63a86f4ce6fab46807aaa5975bdd023df Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 17:02:26 -0700 Subject: [PATCH 20/39] Add documentation for isDuplicate in EventStream --- src/main/java/com/box/sdk/EventStream.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/box/sdk/EventStream.java b/src/main/java/com/box/sdk/EventStream.java index 9cac54bcd..b8e1c6577 100644 --- a/src/main/java/com/box/sdk/EventStream.java +++ b/src/main/java/com/box/sdk/EventStream.java @@ -98,6 +98,14 @@ public void uncaughtException(Thread t, Throwable e) { this.started = true; } + /** + * Indicates whether or not an event ID is a duplicate. + * + *

This method can be overridden by a subclass in order to provide custom de-duping logic.

+ * + * @param eventID the event ID. + * @return true if the event is a duplicate; otherwise false. + */ protected boolean isDuplicate(String eventID) { if (this.receivedEvents == null) { this.receivedEvents = new LinkedHashSet(LRU_SIZE); From cfeafb9e83f21a4d0018859ad29db35dcc02d602 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 17:25:28 -0700 Subject: [PATCH 21/39] Refactor query string building into separate class --- src/main/java/com/box/sdk/BoxFolder.java | 10 +---- .../java/com/box/sdk/QueryStringBuilder.java | 39 +++++++++++++++++++ src/main/java/com/box/sdk/URLTemplate.java | 15 +------ 3 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/box/sdk/QueryStringBuilder.java diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index cad7566c5..9b6c2d6c1 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -35,15 +35,9 @@ public BoxFolder.Info getInfo() { } public BoxFolder.Info getInfo(String... fields) { - StringBuilder fieldsStringBuilder = new StringBuilder(); - for (String field : fields) { - fieldsStringBuilder.append(field); - fieldsStringBuilder.append(","); - } - fieldsStringBuilder.deleteCharAt(fieldsStringBuilder.length() - 1); + String queryString = new QueryStringBuilder().addFieldsParam(fields).toString(); + URL url = FOLDER_INFO_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID()); - String[][] queryParams = new String[][] {{"fields", fieldsStringBuilder.toString()}}; - URL url = FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), queryParams, this.getID()); BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); BoxJSONResponse response = (BoxJSONResponse) request.send(); return new Info(response.getJSON()); diff --git a/src/main/java/com/box/sdk/QueryStringBuilder.java b/src/main/java/com/box/sdk/QueryStringBuilder.java new file mode 100644 index 000000000..97238e7a6 --- /dev/null +++ b/src/main/java/com/box/sdk/QueryStringBuilder.java @@ -0,0 +1,39 @@ +package com.box.sdk; + +class QueryStringBuilder { + private final StringBuilder stringBuilder; + + QueryStringBuilder() { + this.stringBuilder = new StringBuilder(); + } + + QueryStringBuilder addFieldsParam(String... fields) { + StringBuilder fieldsStringBuilder = new StringBuilder(); + for (String field : fields) { + fieldsStringBuilder.append(field); + fieldsStringBuilder.append(","); + } + fieldsStringBuilder.deleteCharAt(fieldsStringBuilder.length() - 1); + + this.addParam("fields", fieldsStringBuilder.toString()); + return this; + } + + QueryStringBuilder addParam(String key, String value) { + if (this.stringBuilder.length() == 0) { + this.stringBuilder.append('?'); + } else { + this.stringBuilder.append('&'); + } + + this.stringBuilder.append(key); + this.stringBuilder.append('='); + this.stringBuilder.append(value); + return this; + } + + @Override + public String toString() { + return this.stringBuilder.toString(); + } +} diff --git a/src/main/java/com/box/sdk/URLTemplate.java b/src/main/java/com/box/sdk/URLTemplate.java index 58caa7254..15208de52 100644 --- a/src/main/java/com/box/sdk/URLTemplate.java +++ b/src/main/java/com/box/sdk/URLTemplate.java @@ -23,19 +23,8 @@ URL build(String base, Object... values) { return url; } - URL build(String base, String[][] queryParams, Object... values) { - String baseURLString = String.format(base + this.template, values); - StringBuilder urlStringBuilder = new StringBuilder(baseURLString); - urlStringBuilder.append("?"); - for (String[] param : queryParams) { - urlStringBuilder.append(param[0]); - urlStringBuilder.append('='); - urlStringBuilder.append(param[1]); - urlStringBuilder.append('&'); - } - urlStringBuilder.deleteCharAt(urlStringBuilder.length() - 1); - - String urlString = urlStringBuilder.toString(); + URL buildWithQuery(String base, String queryString, Object... values) { + String urlString = String.format(base + this.template, values) + queryString; URL url = null; try { url = new URL(urlString); From aec3430c1cd1204488c4beb870d1ff94c57bbf23 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 17:50:18 -0700 Subject: [PATCH 22/39] Add fields support when getting file info --- src/main/java/com/box/sdk/BoxFile.java | 9 +++++++ src/test/java/com/box/sdk/BoxFileTest.java | 28 ++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 0cbe9cf63..bd3887c82 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -51,6 +51,15 @@ public BoxFile.Info getInfo() { return new Info(response.getJSON()); } + public BoxFile.Info getInfo(String... fields) { + String queryString = new QueryStringBuilder().addFieldsParam(fields).toString(); + URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID()); + + BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + return new Info(response.getJSON()); + } + public class Info extends BoxItem.Info { private String sha1; diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 072c81c65..045ec65ea 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -8,7 +8,9 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import org.junit.Test; @@ -34,4 +36,30 @@ public void downloadFileSucceeds() throws UnsupportedEncodingException { uploadedFile.delete(); assertThat(rootFolder, not(hasItem(uploadedFile))); } + + @Test + @Category(IntegrationTest.class) + public void getInfoWithOnlyTheNameField() { + final String expectedName = "[getInfoWithOnlyTheNameField] Test File.txt"; + + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + + final String fileContent = "Test file"; + InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); + BoxFile uploadedFile = rootFolder.uploadFile(stream, expectedName, null, null); + assertThat(rootFolder, hasItem(uploadedFile)); + + BoxFile.Info info = uploadedFile.getInfo("name"); + final String actualName = info.getName(); + final String actualDescription = info.getDescription(); + final long actualSize = info.getSize(); + + assertThat(expectedName, equalTo(actualName)); + assertThat(actualDescription, is(nullValue())); + assertThat(actualSize, is(0L)); + + uploadedFile.delete(); + assertThat(rootFolder, not(hasItem(uploadedFile))); + } } From b6f0f40f4c8b01144dcab3753d16528edc70dd61 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 18 Sep 2014 18:02:10 -0700 Subject: [PATCH 23/39] Add update info for files --- src/main/java/com/box/sdk/BoxFile.java | 9 ++++++++ src/test/java/com/box/sdk/BoxFileTest.java | 25 ++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index bd3887c82..43776194b 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -60,6 +60,15 @@ public BoxFile.Info getInfo(String... fields) { return new Info(response.getJSON()); } + public void updateInfo(BoxFile.Info info) { + URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); + BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT"); + request.setBody(info.getPendingChanges()); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); + info.updateFromJSON(jsonObject); + } + public class Info extends BoxItem.Info { private String sha1; diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 045ec65ea..2fbc3ee22 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -62,4 +62,29 @@ public void getInfoWithOnlyTheNameField() { uploadedFile.delete(); assertThat(rootFolder, not(hasItem(uploadedFile))); } + + @Test + @Category(IntegrationTest.class) + public void updateFileInfoSucceeds() { + final String originalName = "[updateFileInfoSucceeds] Original Name.txt"; + final String newName = "[updateFileInfoSucceeds] New Name.txt"; + + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + + final String fileContent = "Test file"; + InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); + BoxFile uploadedFile = rootFolder.uploadFile(stream, originalName, null, null); + assertThat(rootFolder, hasItem(uploadedFile)); + + BoxFile.Info info = uploadedFile.new Info(); + info.setName(newName); + uploadedFile.updateInfo(info); + + info = uploadedFile.getInfo(); + assertThat(info.getName(), equalTo(newName)); + + uploadedFile.delete(); + assertThat(rootFolder, not(hasItem(uploadedFile))); + } } From 76f7463c0a8cdc98b118a085fd8dd39550e5b2c5 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Fri, 19 Sep 2014 14:26:55 -0700 Subject: [PATCH 24/39] Add ProgressListener interface --- src/main/java/com/box/sdk/ProgressListener.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/com/box/sdk/ProgressListener.java diff --git a/src/main/java/com/box/sdk/ProgressListener.java b/src/main/java/com/box/sdk/ProgressListener.java new file mode 100644 index 000000000..f6c4e584d --- /dev/null +++ b/src/main/java/com/box/sdk/ProgressListener.java @@ -0,0 +1,14 @@ +package com.box.sdk; + +/** + * The listener interface for monitoring the progress of a long-running API call. + */ +public interface ProgressListener { + + /** + * Invoked when the progress of the API call changes. + * @param numBytes the number of bytes completed. + * @param totalBytes the total number of bytes. + */ + void onProgressChanged(long numBytes, long totalBytes); +} From 54f8c425a4683ace07d5afa754f4772a199d5e35 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Fri, 19 Sep 2014 15:33:20 -0700 Subject: [PATCH 25/39] Add a ProgressListener for file downloads --- src/main/java/com/box/sdk/BoxAPIResponse.java | 4 ++++ src/main/java/com/box/sdk/BoxFile.java | 20 +++++++++++++++---- src/test/java/com/box/sdk/BoxFileTest.java | 15 ++++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxAPIResponse.java b/src/main/java/com/box/sdk/BoxAPIResponse.java index f21dc4882..1c8cc3746 100644 --- a/src/main/java/com/box/sdk/BoxAPIResponse.java +++ b/src/main/java/com/box/sdk/BoxAPIResponse.java @@ -44,6 +44,10 @@ public int getResponseCode() { return this.responseCode; } + public long getContentLength() { + return this.connection.getContentLengthLong(); + } + public InputStream getBody() { if (this.inputStream == null) { String contentEncoding = this.connection.getContentEncoding(); diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 43776194b..18e19ae73 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -10,6 +10,7 @@ public class BoxFile extends BoxItem { private static final URLTemplate FILE_URL_TEMPLATE = new URLTemplate("files/%s"); private static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content"); + private static final int BUFFER_SIZE = 8192; private final URL fileURL; private final URL contentURL; @@ -22,15 +23,26 @@ public BoxFile(BoxAPIConnection api, String id) { } public void download(OutputStream output) { + this.download(output, null); + } + + public void download(OutputStream output, ProgressListener listener) { BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.contentURL, "GET"); BoxAPIResponse response = request.send(); InputStream input = response.getBody(); + long totalRead = 0; + byte[] buffer = new byte[BUFFER_SIZE]; try { - int b = input.read(); - while (b != -1) { - output.write(b); - b = input.read(); + int n = input.read(buffer); + totalRead += n; + while (n != -1) { + output.write(buffer, 0, n); + if (listener != null) { + listener.onProgressChanged(totalRead, response.getContentLength()); + } + n = input.read(buffer); + totalRead += n; } } catch (IOException e) { throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 2fbc3ee22..5ae2e213b 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -24,14 +24,25 @@ public void downloadFileSucceeds() throws UnsupportedEncodingException { BoxFolder rootFolder = BoxFolder.getRootFolder(api); final String fileContent = "Test file"; + final long fileLength = fileContent.length(); InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); BoxFile uploadedFile = rootFolder.uploadFile(stream, "Test File.txt", null, null); assertThat(rootFolder, hasItem(uploadedFile)); ByteArrayOutputStream output = new ByteArrayOutputStream(); - uploadedFile.download(output); + final boolean[] onProgressChangedCalled = new boolean[]{false}; + uploadedFile.download(output, new ProgressListener() { + public void onProgressChanged(long numBytes, long totalBytes) { + onProgressChangedCalled[0] = true; + + assertThat(numBytes, is(not(0L))); + assertThat(totalBytes, is(equalTo(fileLength))); + } + }); + + assertThat(onProgressChangedCalled[0], is(true)); String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); - assertThat(fileContent, equalTo(downloadedFileContent)); + assertThat(downloadedFileContent, equalTo(fileContent)); uploadedFile.delete(); assertThat(rootFolder, not(hasItem(uploadedFile))); From f65174a4d04dceaa9e05ca5254ecab6b320732dc Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Mon, 22 Sep 2014 13:28:30 -0700 Subject: [PATCH 26/39] Implement created/modified timestamps for uploads --- src/main/java/com/box/sdk/BoxFolder.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index 9b6c2d6c1..05c3845c7 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -128,12 +128,24 @@ public void rename(String newName) { response.disconnect(); } + public BoxFile uploadFile(InputStream fileContent, String name) { + return this.uploadFile(fileContent, name, null, null); + } + public BoxFile uploadFile(InputStream fileContent, String name, Date created, Date modified) { URL uploadURL = UPLOAD_FILE_URL.build(UPLOAD_FILE_URL_BASE); BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL); request.putField("parent_id", getID()); request.setFile(fileContent, name); + if (created != null) { + request.putField("content_created_at", created); + } + + if (modified != null) { + request.putField("content_modified_at", modified); + } + BoxJSONResponse response = (BoxJSONResponse) request.send(); JsonObject collection = JsonObject.readFrom(response.getJSON()); JsonArray entries = collection.get("entries").asArray(); From 83226b34cd67f0b4ab900f23da440aed58c9f37c Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Mon, 22 Sep 2014 13:38:32 -0700 Subject: [PATCH 27/39] Line wrap overview documentation --- doc/overview.md | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/doc/overview.md b/doc/overview.md index 90e2eaebf..66ecdc3ff 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -1,29 +1,41 @@ SDK Overview ============ -This guide covers the basics behind the various components of the Box Java SDK. It's also recommended that you take a look at [the documentation](https://developers.box.com/docs/) for the Box API. +This guide covers the basics behind the various components of the Box Java SDK. +It's also recommended that you take a look at [the +documentation](https://developers.box.com/docs/) for the Box API. API Connections --------------- -The first step in using the SDK is always authenticating and connecting to the API. The SDK does this through the `BoxAPIConnection` class. This class represents an authenticated connection to a specific version of the Box API. It is responsible for things such as: +The first step in using the SDK is always authenticating and connecting to the +API. The SDK does this through the `BoxAPIConnection` class. This class +represents an authenticated connection to a specific version of the Box API. It +is responsible for things such as: * Storing authentication information * Automatic token refresh * Handling rate-limiting and exponential backoff -You can also create more than one `BoxAPIConnection`. For example, you can have a connection for each user if your application supports multiple user accounts. +You can also create more than one `BoxAPIConnection`. For example, you can have +a connection for each user if your application supports multiple user accounts. -See the [Authentication guide](authentication.md) for details on how to create and use `BoxAPIConnection`. +See the [Authentication guide](authentication.md) for details on how to create +and use `BoxAPIConnection`. Requests and Responses ---------------------- -All communication with Box's API is done through `BoxAPIRequest` and `BoxAPIResponse` (or their subclasses). These classes handle all the dirty work of setting appropriate headers, handling errors, and sending/receiving data. +All communication with Box's API is done through `BoxAPIRequest` and +`BoxAPIResponse` (or their subclasses). These classes handle all the dirty work +of setting appropriate headers, handling errors, and sending/receiving data. -You generally won't need to use these classes directly, as the resource types are easier and cover most use-cases. However, these classes are extremely flexible and can be used if you need to make custom API calls. +You generally won't need to use these classes directly, as the resource types +are easier and cover most use-cases. However, these classes are extremely +flexible and can be used if you need to make custom API calls. -Here's an example using `BoxAPIRequest` and `BoxJSONResponse` that gets a list of items with some custom fields: +Here's an example using `BoxAPIRequest` and `BoxJSONResponse` that gets a list +of items with some custom fields: ```java BoxAPIConnection api = new BoxAPIConnection("token"); @@ -36,7 +48,10 @@ String json = response.getJSON(); Resource Types -------------- -Resources types are the classes you'll use the most. Things like `BoxFile`, `BoxFolder`, `BoxUser`, etc. are all resource types. A resource always has an ID and an associated API connection. Instantiating and using a resource type is simple: +Resources types are the classes you'll use the most. Things like `BoxFile`, +`BoxFolder`, `BoxUser`, etc. are all resource types. A resource always has an ID +and an associated API connection. Instantiating and using a resource type is +simple: ```java // Print the name of the folder with ID "1234". @@ -45,7 +60,9 @@ BoxFolder.Info info = folder.getInfo(); System.out.println(info.getName()); ``` -A resource type will always have the same API connection as the type that instantiated it. For example, `creator` will have the same API connection that `folder` does. +A resource type will always have the same API connection as the type that +instantiated it. For example, `creator` will have the same API connection that +`folder` does. ```java BoxFolder folder = new BoxFolder(api, "1234") From 80f7a93ac10e6acc31d6c5e25c4e6a435a3d69fb Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Mon, 22 Sep 2014 13:40:56 -0700 Subject: [PATCH 28/39] Fix info about BoxAPIConnection in overview docs --- doc/overview.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/overview.md b/doc/overview.md index 66ecdc3ff..5bbacca70 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -13,9 +13,10 @@ API. The SDK does this through the `BoxAPIConnection` class. This class represents an authenticated connection to a specific version of the Box API. It is responsible for things such as: -* Storing authentication information -* Automatic token refresh -* Handling rate-limiting and exponential backoff +* Storing authentication information. +* Automatically refreshing tokens. +* Configuring rate-limiting, number of retry attempts and other connection + settings. You can also create more than one `BoxAPIConnection`. For example, you can have a connection for each user if your application supports multiple user accounts. From b403e0ed42fa554fab9b61d0be387a245dcdecfa Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Mon, 22 Sep 2014 13:46:30 -0700 Subject: [PATCH 29/39] Line wrap authentication documentation --- doc/authentication.md | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/doc/authentication.md b/doc/authentication.md index 8ee1a90d0..4fc5f9c70 100644 --- a/doc/authentication.md +++ b/doc/authentication.md @@ -1,14 +1,21 @@ Authentication ============== -The Box API uses OAuth2 for authentication, which can be difficult to implement. The SDK makes it easier by providing classes that handle obtaining tokens and automatically refreshing them. +The Box API uses OAuth2 for authentication, which can be difficult to implement. +The SDK makes it easier by providing classes that handle obtaining tokens and +automatically refreshing them. Ways to Authenticate -------------------- ### Developer Tokens -The fastest way to get started using the API is with developer tokens. A developer token is simply a short-lived access token that cannot be refreshed and can only be used with your own account. Therefore, they're only useful for testing an app and aren't suitable for production. You can obtain a developer token from your application's [developer console](https://cloud.app.box.com/developers/services). +The fastest way to get started using the API is with developer tokens. A +developer token is simply a short-lived access token that cannot be refreshed +and can only be used with your own account. Therefore, they're only useful for +testing an app and aren't suitable for production. You can obtain a developer +token from your application's [developer +console](https://cloud.app.box.com/developers/services). The following example creates an API connection with a developer token: @@ -18,9 +25,14 @@ BoxAPIConnection api = new BoxAPIConnection("YOUR-DEVELOPER-TOKEN"); ### Normal Authentication -Using an auth code is the most common way of authenticating with the Box API. Your application must provide a way for the user to login to Box (usually with a browser or web view) in order to obtain an auth code. +Using an auth code is the most common way of authenticating with the Box API. +Your application must provide a way for the user to login to Box (usually with a +browser or web view) in order to obtain an auth code. -After a user logs in and grants your application access to their Box account, they will be redirected to your application's `redirect_uri` which will contain an auth code. This auth code can then be used along with your client ID and client secret to establish an API connection. +After a user logs in and grants your application access to their Box account, +they will be redirected to your application's `redirect_uri` which will contain +an auth code. This auth code can then be used along with your client ID and +client secret to establish an API connection. ```java BoxAPIConnection api = new BoxAPIConnection("YOUR-CLIENT-ID", "YOUR-CLIENT-SECRET", "YOUR-AUTH-CODE"); @@ -28,7 +40,9 @@ BoxAPIConnection api = new BoxAPIConnection("YOUR-CLIENT-ID", "YOUR-CLIENT-SECRE ### Manual Authentication -In certain advanced scenarios, you may want to obtain an access and refresh token yourself through manual calls to the API. In this case, you can create an API connection with the tokens directly. +In certain advanced scenarios, you may want to obtain an access and refresh +token yourself through manual calls to the API. In this case, you can create an +API connection with the tokens directly. ```java BoxAPIConnection api = new BoxAPIConnection("YOUR-CLIENT-ID", "YOUR-CLIENT-SECRET", "YOUR-ACCESS-TOKEN", @@ -38,7 +52,10 @@ BoxAPIConnection api = new BoxAPIConnection("YOUR-CLIENT-ID", "YOUR-CLIENT-SECRE Auto-Refresh ------------ -By default, a `BoxAPIConnection` will automatically refresh the access token if it has expired. To disable auto-refresh, set the connection's refresh token to null. Keep in mind that you will have to manually refresh and update the access token yourself. +By default, a `BoxAPIConnection` will automatically refresh the access token if +it has expired. To disable auto-refresh, set the connection's refresh token to +null. Keep in mind that you will have to manually refresh and update the access +token yourself. ```java // This connection won't auto-refresh. From e7f5d5f17816ab83f009778144f9df83b274f035 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Mon, 22 Sep 2014 14:22:14 -0700 Subject: [PATCH 30/39] Add guide for resource types --- doc/resource-types.md | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 doc/resource-types.md diff --git a/doc/resource-types.md b/doc/resource-types.md new file mode 100644 index 000000000..c7cd908d4 --- /dev/null +++ b/doc/resource-types.md @@ -0,0 +1,83 @@ +SDK Resource Types +================== + +All resources require a `BoxAPIConnection` in order to communicate with the Box +API. For more information on how to create an API connection, see the +[Authentication Guide](authentication.md). + +* [Files](#files) + +Files +----- + +File objects represent individual files in Box. + +* [Javadoc Documentation](https://gitenterprise.inside-box.net/pages/Box/box-java-sdk/javadoc/com/box/sdk/BoxFile.html) +* [REST API Documentation](https://developers.box.com/docs/#files) + +### Get a File's Information + +```java +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info info = file.getInfo(); +``` + +#### Only Get Information for Specific Fields + +```java +BoxFile file = new BoxFile(api, "id"); +// Only get information about a few specific fields. +BoxFile.Info info = file.getInfo("size", "owned_by"); +``` + +### Update a File's Information + +```java +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info info = file.new Info(); +info.setName("New Name"); +file.updateInfo(info); +``` + +### Download a File + +```java +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info info = file.getInfo(); + +FileOutputStream stream = new FileOutputStream(info.getName()); +file.download(stream); +stream.close(); +``` + +#### Track the Progress of a Download + +```java +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info info = file.getInfo(); + +FileOutputStream stream = new FileOutputStream(info.getName()); +// Provide a ProgressListener to monitor the progress of the download. +file.download(stream, new ProgressListener() { + public void onProgressChanged(long numBytes, long totalBytes) { + double percentComplete = numBytes / totalBytes; + } +}); +stream.close(); +``` + +### Upload a File + +```java +BoxFolder rootFolder = BoxFolder.getRootFolder(api); +FileInputStream stream = new FileInputStream("My File.txt"); +rootFolder.uploadFile(stream, "My File.txt"); +stream.close(); +``` + +### Delete a File + +```java +BoxFile file = new BoxFile(api, "id"); +file.delete(); +``` From 75643a5b52d1030831f87b3d1108d5023cf2c48b Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Mon, 22 Sep 2014 18:28:49 -0700 Subject: [PATCH 31/39] Implement uploading and retrieving file versions --- .../java/com/box/sdk/BoxAPIConnection.java | 19 +++++ src/main/java/com/box/sdk/BoxFile.java | 38 +++++++++ src/main/java/com/box/sdk/BoxFileVersion.java | 81 +++++++++++++++++++ src/main/java/com/box/sdk/BoxItem.java | 2 +- .../java/com/box/sdk/BoxItemIterator.java | 6 +- src/main/java/com/box/sdk/BoxUser.java | 6 +- src/test/java/com/box/sdk/BoxFileTest.java | 36 +++++++++ 7 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/box/sdk/BoxFileVersion.java diff --git a/src/main/java/com/box/sdk/BoxAPIConnection.java b/src/main/java/com/box/sdk/BoxAPIConnection.java index 49bd9d60a..2de9f3e3a 100644 --- a/src/main/java/com/box/sdk/BoxAPIConnection.java +++ b/src/main/java/com/box/sdk/BoxAPIConnection.java @@ -17,6 +17,7 @@ public class BoxAPIConnection { private static final String TOKEN_URL_STRING = "https://www.box.com/api/oauth2/token"; private static final String DEFAULT_BASE_URL = "https://api.box.com/2.0/"; + private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/2.0/"; /** * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For @@ -30,6 +31,7 @@ public class BoxAPIConnection { private long lastRefresh; private long expires; private String baseURL; + private String baseUploadURL; private String accessToken; private String refreshToken; private boolean autoRefresh; @@ -56,6 +58,7 @@ public BoxAPIConnection(String clientID, String clientSecret, String accessToken this.accessToken = accessToken; this.setRefreshToken(refreshToken); this.baseURL = DEFAULT_BASE_URL; + this.baseUploadURL = DEFAULT_BASE_UPLOAD_URL; this.autoRefresh = true; this.maxRequestAttempts = DEFAULT_MAX_ATTEMPTS; } @@ -127,6 +130,22 @@ public void setBaseURL(String baseURL) { this.baseURL = baseURL; } + /** + * Gets the base upload URL that's used when performing file uploads to Box. + * @return the base upload URL. + */ + public String getBaseUploadURL() { + return this.baseUploadURL; + } + + /** + * Sets the base upload URL to be used when performing file uploads to Box. + * @param baseUploadURL a base upload URL. + */ + public void setBaseUploadURL(String baseUploadURL) { + this.baseUploadURL = baseUploadURL; + } + /** * 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(). diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 18e19ae73..66b47d4a4 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -4,12 +4,18 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; public class BoxFile extends BoxItem { private static final URLTemplate FILE_URL_TEMPLATE = new URLTemplate("files/%s"); private static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content"); + private static final URLTemplate VERSIONS_URL_TEMPLATE = new URLTemplate("/files/%s/versions"); private static final int BUFFER_SIZE = 8192; private final URL fileURL; @@ -81,6 +87,38 @@ public void updateInfo(BoxFile.Info info) { info.updateFromJSON(jsonObject); } + public Collection getVersions() { + URL url = VERSIONS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); + BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + + JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); + JsonArray entries = jsonObject.get("entries").asArray(); + Collection versions = new ArrayList(); + for (JsonValue entry : entries) { + versions.add(new BoxFileVersion(this.getAPI(), entry.asObject())); + } + + return versions; + } + + public void uploadVersion(InputStream fileContent) { + this.uploadVersion(fileContent, null); + } + + public void uploadVersion(InputStream fileContent, Date modified) { + URL uploadURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID()); + BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL); + request.setFile(fileContent, ""); + + if (modified != null) { + request.putField("content_modified_at", modified); + } + + BoxAPIResponse response = request.send(); + response.disconnect(); + } + public class Info extends BoxItem.Info { private String sha1; diff --git a/src/main/java/com/box/sdk/BoxFileVersion.java b/src/main/java/com/box/sdk/BoxFileVersion.java new file mode 100644 index 000000000..5b1532a11 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxFileVersion.java @@ -0,0 +1,81 @@ +package com.box.sdk; + +import java.text.ParseException; +import java.util.Date; + +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +public class BoxFileVersion extends BoxResource { + private String sha1; + private String name; + private long size; + private Date createdAt; + private Date modifiedAt; + private BoxUser.Info modifiedBy; + + BoxFileVersion(BoxAPIConnection api, JsonObject jsonObject) { + super(api, jsonObject.get("id").asString()); + + for (JsonObject.Member member : jsonObject) { + JsonValue value = member.getValue(); + if (value.isNull()) { + continue; + } + + try { + switch (member.getName()) { + case "sha1": + this.sha1 = value.asString(); + break; + case "name": + this.name = value.asString(); + break; + case "size": + this.size = Double.valueOf(value.toString()).longValue(); + break; + case "created_at": + this.createdAt = BoxDateParser.parse(value.asString()); + break; + case "modified_at": + this.modifiedAt = BoxDateParser.parse(value.asString()); + break; + case "modified_by": + JsonObject userJSON = value.asObject(); + String userID = userJSON.get("id").asString(); + BoxUser user = new BoxUser(getAPI(), userID); + this.modifiedBy = user.new Info(userJSON); + break; + default: + break; + } + } catch (ParseException e) { + assert false : "A ParseException indicates a bug in the SDK."; + } + } + } + + public String getSha1() { + return this.sha1; + } + + public String getName() { + return this.name; + } + + public long getSize() { + return this.size; + } + + public Date getCreatedAt() { + return this.createdAt; + } + + public Date getModifiedAt() { + return this.modifiedAt; + } + + public BoxUser.Info getModifiedBy() { + return this.modifiedBy; + } +} diff --git a/src/main/java/com/box/sdk/BoxItem.java b/src/main/java/com/box/sdk/BoxItem.java index 83f3aae14..f613b2687 100644 --- a/src/main/java/com/box/sdk/BoxItem.java +++ b/src/main/java/com/box/sdk/BoxItem.java @@ -149,7 +149,7 @@ protected void parseJSONMember(JsonObject.Member member) { this.description = value.asString(); break; case "size": - this.size = value.asLong(); + this.size = Double.valueOf(value.toString()).longValue(); break; case "trashed_at": this.trashedAt = BoxDateParser.parse(value.asString()); diff --git a/src/main/java/com/box/sdk/BoxItemIterator.java b/src/main/java/com/box/sdk/BoxItemIterator.java index 3343d6f30..f6272dcd5 100644 --- a/src/main/java/com/box/sdk/BoxItemIterator.java +++ b/src/main/java/com/box/sdk/BoxItemIterator.java @@ -72,8 +72,10 @@ private void loadNextPage() { String json = response.getJSON(); JsonObject jsonObject = JsonObject.readFrom(json); - this.totalCount = jsonObject.get("total_count").asLong(); - this.offset = jsonObject.get("offset").asLong(); + String totalCountString = jsonObject.get("total_count").toString(); + this.totalCount = Double.valueOf(totalCountString).longValue(); + String offsetString = jsonObject.get("offset").toString(); + this.offset = Double.valueOf(offsetString).longValue(); this.hasMorePages = (this.offset + LIMIT) < this.totalCount; JsonArray jsonArray = jsonObject.get("entries").asArray(); diff --git a/src/main/java/com/box/sdk/BoxUser.java b/src/main/java/com/box/sdk/BoxUser.java index 8196a2517..e337f88e7 100644 --- a/src/main/java/com/box/sdk/BoxUser.java +++ b/src/main/java/com/box/sdk/BoxUser.java @@ -157,13 +157,13 @@ protected void parseJSONMember(JsonObject.Member member) { this.timezone = value.asString(); break; case "space_amount": - this.spaceAmount = value.asLong(); + this.spaceAmount = Double.valueOf(value.toString()).longValue(); break; case "space_used": - this.spaceUsed = value.asLong(); + this.spaceUsed = Double.valueOf(value.toString()).longValue(); break; case "max_upload_size": - this.maxUploadSize = value.asLong(); + this.maxUploadSize = Double.valueOf(value.toString()).longValue(); break; case "status": this.status = this.parseStatus(value); diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 5ae2e213b..669b79ad9 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -5,9 +5,12 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import java.util.Collection; +import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -98,4 +101,37 @@ public void updateFileInfoSucceeds() { uploadedFile.delete(); assertThat(rootFolder, not(hasItem(uploadedFile))); } + + @Test + @Category(IntegrationTest.class) + public void uploadMultipleVersionsSucceeds() throws UnsupportedEncodingException { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + + final String fileName = "[uploadMultipleVersionsSucceeds] Multi-version File.txt"; + final String version1Content = "Version 1"; + final String version1Sha = "db3cbc01da600701b9fe4a497fe328e71fa7022f"; + final String version2Content = "Version 2"; + + InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); + BoxFile file = rootFolder.uploadFile(stream, fileName); + assertThat(rootFolder, hasItem(file)); + + stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); + file.uploadVersion(stream); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + file.download(output); + String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); + assertThat(downloadedFileContent, equalTo(version2Content)); + + Collection versions = file.getVersions(); + assertThat(versions, hasSize(1)); + + BoxFileVersion previousVersion = versions.iterator().next(); + assertThat(previousVersion.getSha1(), is(equalTo(version1Sha))); + + file.delete(); + assertThat(rootFolder, not(hasItem(file))); + } } From 8277fdfb12dff7266f4c8d4db90feced05fce0fb Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 23 Sep 2014 15:45:51 -0700 Subject: [PATCH 32/39] Add delete file version --- src/main/java/com/box/sdk/BoxFile.java | 2 +- src/main/java/com/box/sdk/BoxFileVersion.java | 15 +++++++++- src/test/java/com/box/sdk/BoxFileTest.java | 28 ++++++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 66b47d4a4..8c18dd9b0 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -96,7 +96,7 @@ public Collection getVersions() { JsonArray entries = jsonObject.get("entries").asArray(); Collection versions = new ArrayList(); for (JsonValue entry : entries) { - versions.add(new BoxFileVersion(this.getAPI(), entry.asObject())); + versions.add(new BoxFileVersion(this.getAPI(), entry.asObject(), this.getID())); } return versions; diff --git a/src/main/java/com/box/sdk/BoxFileVersion.java b/src/main/java/com/box/sdk/BoxFileVersion.java index 5b1532a11..738600eb1 100644 --- a/src/main/java/com/box/sdk/BoxFileVersion.java +++ b/src/main/java/com/box/sdk/BoxFileVersion.java @@ -1,5 +1,6 @@ package com.box.sdk; +import java.net.URL; import java.text.ParseException; import java.util.Date; @@ -7,6 +8,10 @@ import com.eclipsesource.json.JsonValue; public class BoxFileVersion extends BoxResource { + private static final URLTemplate VERSION_URL_TEMPLATE = new URLTemplate("/files/%s/versions/%s"); + + private final String fileID; + private String sha1; private String name; private long size; @@ -14,9 +19,10 @@ public class BoxFileVersion extends BoxResource { private Date modifiedAt; private BoxUser.Info modifiedBy; - BoxFileVersion(BoxAPIConnection api, JsonObject jsonObject) { + BoxFileVersion(BoxAPIConnection api, JsonObject jsonObject, String fileID) { super(api, jsonObject.get("id").asString()); + this.fileID = fileID; for (JsonObject.Member member : jsonObject) { JsonValue value = member.getValue(); if (value.isNull()) { @@ -78,4 +84,11 @@ public Date getModifiedAt() { public BoxUser.Info getModifiedBy() { return this.modifiedBy; } + + public void delete() { + URL url = VERSION_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.fileID, this.getID()); + BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE"); + BoxAPIResponse response = request.send(); + response.disconnect(); + } } diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 669b79ad9..47785a8ff 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -7,7 +7,6 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; -import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; @@ -134,4 +133,31 @@ public void uploadMultipleVersionsSucceeds() throws UnsupportedEncodingException file.delete(); assertThat(rootFolder, not(hasItem(file))); } + + @Test + @Category(IntegrationTest.class) + public void deleteVersionDoesNotThrowException() throws UnsupportedEncodingException { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + + final String fileName = "[deleteVersionSucceeds] Multi-version File.txt"; + final String version1Content = "Version 1"; + final String version2Content = "Version 2"; + + InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); + BoxFile file = rootFolder.uploadFile(stream, fileName); + assertThat(rootFolder, hasItem(file)); + + stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); + file.uploadVersion(stream); + + Collection versions = file.getVersions(); + assertThat(versions, hasSize(1)); + + BoxFileVersion previousVersion = versions.iterator().next(); + previousVersion.delete(); + + file.delete(); + assertThat(rootFolder, not(hasItem(file))); + } } From 890e8f342bfaf6ca41afa53cf27adb2a2f434f93 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 23 Sep 2014 16:30:30 -0700 Subject: [PATCH 33/39] Add version downloading --- src/main/java/com/box/sdk/BoxFile.java | 2 +- src/main/java/com/box/sdk/BoxFileVersion.java | 37 ++++++++++++++++- src/test/java/com/box/sdk/BoxFileTest.java | 40 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 8c18dd9b0..122e4a826 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -15,7 +15,7 @@ public class BoxFile extends BoxItem { private static final URLTemplate FILE_URL_TEMPLATE = new URLTemplate("files/%s"); private static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content"); - private static final URLTemplate VERSIONS_URL_TEMPLATE = new URLTemplate("/files/%s/versions"); + private static final URLTemplate VERSIONS_URL_TEMPLATE = new URLTemplate("files/%s/versions"); private static final int BUFFER_SIZE = 8192; private final URL fileURL; diff --git a/src/main/java/com/box/sdk/BoxFileVersion.java b/src/main/java/com/box/sdk/BoxFileVersion.java index 738600eb1..2debf4bb4 100644 --- a/src/main/java/com/box/sdk/BoxFileVersion.java +++ b/src/main/java/com/box/sdk/BoxFileVersion.java @@ -1,5 +1,8 @@ package com.box.sdk; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URL; import java.text.ParseException; import java.util.Date; @@ -8,7 +11,9 @@ import com.eclipsesource.json.JsonValue; public class BoxFileVersion extends BoxResource { - private static final URLTemplate VERSION_URL_TEMPLATE = new URLTemplate("/files/%s/versions/%s"); + private static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content?version=%s"); + private static final URLTemplate VERSION_URL_TEMPLATE = new URLTemplate("files/%s/versions/%s"); + private static final int BUFFER_SIZE = 8192; private final String fileID; @@ -91,4 +96,34 @@ public void delete() { BoxAPIResponse response = request.send(); response.disconnect(); } + + public void download(OutputStream output) { + this.download(output, null); + } + + public void download(OutputStream output, ProgressListener listener) { + URL url = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.fileID, this.getID()); + BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); + BoxAPIResponse response = request.send(); + InputStream input = response.getBody(); + + long totalRead = 0; + byte[] buffer = new byte[BUFFER_SIZE]; + try { + int n = input.read(buffer); + totalRead += n; + while (n != -1) { + output.write(buffer, 0, n); + if (listener != null) { + listener.onProgressChanged(totalRead, response.getContentLength()); + } + n = input.read(buffer); + totalRead += n; + } + } catch (IOException e) { + throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); + } + + response.disconnect(); + } } diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 47785a8ff..f0d5bb664 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -50,6 +50,46 @@ public void onProgressChanged(long numBytes, long totalBytes) { assertThat(rootFolder, not(hasItem(uploadedFile))); } + @Test + @Category(IntegrationTest.class) + public void downloadVersionSucceeds() throws UnsupportedEncodingException { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + + final String fileName = "[downloadVersionSucceeds] Multi-version File.txt"; + final String version1Content = "Version 1"; + final long version1Length = version1Content.length(); + final String version2Content = "Version 2"; + + InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); + BoxFile uploadedFile = rootFolder.uploadFile(stream, fileName); + assertThat(rootFolder, hasItem(uploadedFile)); + + stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); + uploadedFile.uploadVersion(stream); + + Collection versions = uploadedFile.getVersions(); + BoxFileVersion previousVersion = versions.iterator().next(); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + final boolean[] onProgressChangedCalled = new boolean[]{false}; + previousVersion.download(output, new ProgressListener() { + public void onProgressChanged(long numBytes, long totalBytes) { + onProgressChangedCalled[0] = true; + + assertThat(numBytes, is(not(0L))); + assertThat(totalBytes, is(equalTo(version1Length))); + } + }); + + assertThat(onProgressChangedCalled[0], is(true)); + String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); + assertThat(downloadedFileContent, equalTo(version1Content)); + + uploadedFile.delete(); + assertThat(rootFolder, not(hasItem(uploadedFile))); + } + @Test @Category(IntegrationTest.class) public void getInfoWithOnlyTheNameField() { From 3d7f08a18d8bff2886817148d9efb127ad48467b Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 23 Sep 2014 17:12:19 -0700 Subject: [PATCH 34/39] Add file version promotion --- src/main/java/com/box/sdk/BoxFileVersion.java | 13 ++++++++ src/test/java/com/box/sdk/BoxFileTest.java | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFileVersion.java b/src/main/java/com/box/sdk/BoxFileVersion.java index 2debf4bb4..30304bac3 100644 --- a/src/main/java/com/box/sdk/BoxFileVersion.java +++ b/src/main/java/com/box/sdk/BoxFileVersion.java @@ -126,4 +126,17 @@ public void download(OutputStream output, ProgressListener listener) { response.disconnect(); } + + public void promote() { + URL url = VERSION_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.fileID, "current"); + + JsonObject jsonObject = new JsonObject(); + jsonObject.add("type", "file_version"); + jsonObject.add("id", this.getID()); + + BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST"); + request.setBody(jsonObject.toString()); + BoxAPIResponse response = request.send(); + response.disconnect(); + } } diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index f0d5bb664..236820efb 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -200,4 +200,34 @@ public void deleteVersionDoesNotThrowException() throws UnsupportedEncodingExcep file.delete(); assertThat(rootFolder, not(hasItem(file))); } + + @Test + @Category(IntegrationTest.class) + public void promoteVersionsSucceeds() throws UnsupportedEncodingException { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + + final String fileName = "[promoteVersionsSucceeds] Multi-version File.txt"; + final String version1Content = "Version 1"; + final String version2Content = "Version 2"; + + InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); + BoxFile file = rootFolder.uploadFile(stream, fileName); + assertThat(rootFolder, hasItem(file)); + + stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); + file.uploadVersion(stream); + + Collection versions = file.getVersions(); + BoxFileVersion previousVersion = versions.iterator().next(); + previousVersion.promote(); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + file.download(output); + String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); + assertThat(downloadedFileContent, equalTo(version1Content)); + + file.delete(); + assertThat(rootFolder, not(hasItem(file))); + } } From d5e86554884c67d7373bc6cc0bfa0cba13fdc1c8 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Tue, 23 Sep 2014 17:35:58 -0700 Subject: [PATCH 35/39] Add file copy --- src/main/java/com/box/sdk/BoxFile.java | 29 ++++++++++++++++++++++ src/test/java/com/box/sdk/BoxFileTest.java | 24 ++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 122e4a826..1438e7323 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -16,6 +16,7 @@ public class BoxFile extends BoxItem { private static final URLTemplate FILE_URL_TEMPLATE = new URLTemplate("files/%s"); private static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content"); private static final URLTemplate VERSIONS_URL_TEMPLATE = new URLTemplate("files/%s/versions"); + private static final URLTemplate COPY_URL_TEMPLATE = new URLTemplate("files/%s/copy"); private static final int BUFFER_SIZE = 8192; private final URL fileURL; @@ -57,6 +58,30 @@ public void download(OutputStream output, ProgressListener listener) { response.disconnect(); } + public BoxFile.Info copy(BoxFolder destination) { + return this.copy(destination, null); + } + + public BoxFile.Info copy(BoxFolder destination, String newName) { + URL url = COPY_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); + + JsonObject parent = new JsonObject(); + parent.add("id", destination.getID()); + + JsonObject copyInfo = new JsonObject(); + copyInfo.add("parent", parent); + if (newName != null) { + copyInfo.add("name", newName); + } + + BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST"); + request.setBody(copyInfo.toString()); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); + BoxFile copiedFile = new BoxFile(this.getAPI(), responseJSON.get("id").asString()); + return copiedFile.new Info(responseJSON); + } + public void delete() { BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.fileURL, "DELETE"); BoxAPIResponse response = request.send(); @@ -130,6 +155,10 @@ public Info(String json) { super(json); } + protected Info(JsonObject jsonObject) { + super(jsonObject); + } + @Override public BoxFile getResource() { return BoxFile.this; diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 236820efb..7cfd6ff4e 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -230,4 +230,28 @@ public void promoteVersionsSucceeds() throws UnsupportedEncodingException { file.delete(); assertThat(rootFolder, not(hasItem(file))); } + + @Test + @Category(IntegrationTest.class) + public void copyFileSucceeds() throws UnsupportedEncodingException { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + + final String originalName = "[copyFileSucceeds] Original File.txt"; + final String newName = "[copyFileSucceeds] New File.txt"; + final String fileContent = "Test file"; + + InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); + BoxFile uploadedFile = rootFolder.uploadFile(stream, originalName); + assertThat(rootFolder, hasItem(uploadedFile)); + + BoxFile.Info copiedFileInfo = uploadedFile.copy(rootFolder, newName); + BoxFile copiedFile = copiedFileInfo.getResource(); + assertThat(rootFolder, hasItem(copiedFile)); + + uploadedFile.delete(); + assertThat(rootFolder, not(hasItem(uploadedFile))); + copiedFile.delete(); + assertThat(rootFolder, not(hasItem(copiedFile))); + } } From 5a1290230f19447a03fe9e28cb68f4f49d314923 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Wed, 24 Sep 2014 17:04:46 -0700 Subject: [PATCH 36/39] Clean up file tests --- src/test/java/com/box/sdk/BoxFileTest.java | 249 ++++++++------------- 1 file changed, 96 insertions(+), 153 deletions(-) diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 7cfd6ff4e..e0b9feee5 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -11,9 +11,12 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.longThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -21,184 +24,126 @@ public class BoxFileTest { @Test @Category(IntegrationTest.class) - public void downloadFileSucceeds() throws UnsupportedEncodingException { + public void uploadAndDownloadFileSucceeds() throws UnsupportedEncodingException { BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); BoxFolder rootFolder = BoxFolder.getRootFolder(api); + String fileName = "[uploadAndDownloadFileSucceeds] Test File.txt"; + String fileContent = "Non-empty string"; + byte[] fileBytes = fileContent.getBytes(StandardCharsets.UTF_8); + long fileSize = fileBytes.length; - final String fileContent = "Test file"; - final long fileLength = fileContent.length(); - InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); - BoxFile uploadedFile = rootFolder.uploadFile(stream, "Test File.txt", null, null); - assertThat(rootFolder, hasItem(uploadedFile)); - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - final boolean[] onProgressChangedCalled = new boolean[]{false}; - uploadedFile.download(output, new ProgressListener() { - public void onProgressChanged(long numBytes, long totalBytes) { - onProgressChangedCalled[0] = true; + InputStream uploadStream = new ByteArrayInputStream(fileBytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); - assertThat(numBytes, is(not(0L))); - assertThat(totalBytes, is(equalTo(fileLength))); - } - }); + ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); + ProgressListener mockProgressListener = mock(ProgressListener.class); + uploadedFile.download(downloadStream, mockProgressListener); + String downloadedContent = downloadStream.toString(StandardCharsets.UTF_8.name()); - assertThat(onProgressChangedCalled[0], is(true)); - String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); - assertThat(downloadedFileContent, equalTo(fileContent)); + assertThat(rootFolder, hasItem(uploadedFile)); + assertThat(downloadedContent, equalTo(fileContent)); + verify(mockProgressListener).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize)))); uploadedFile.delete(); - assertThat(rootFolder, not(hasItem(uploadedFile))); } @Test @Category(IntegrationTest.class) - public void downloadVersionSucceeds() throws UnsupportedEncodingException { + public void uploadAndDownloadMultipleVersionsSucceeds() throws UnsupportedEncodingException { BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); BoxFolder rootFolder = BoxFolder.getRootFolder(api); - - final String fileName = "[downloadVersionSucceeds] Multi-version File.txt"; - final String version1Content = "Version 1"; - final long version1Length = version1Content.length(); - final String version2Content = "Version 2"; - - InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); - BoxFile uploadedFile = rootFolder.uploadFile(stream, fileName); - assertThat(rootFolder, hasItem(uploadedFile)); - - stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); - uploadedFile.uploadVersion(stream); + String fileName = "[uploadAndDownloadMultipleVersionsSucceeds] Multi-version File.txt"; + String version1Content = "Version 1"; + String version1Sha = "db3cbc01da600701b9fe4a497fe328e71fa7022f"; + byte[] version1Bytes = version1Content.getBytes(StandardCharsets.UTF_8); + long version1Size = version1Bytes.length; + String version2Content = "Version 2"; + byte[] version2Bytes = version2Content.getBytes(StandardCharsets.UTF_8); + long version2Size = version1Bytes.length; + + InputStream uploadStream = new ByteArrayInputStream(version1Bytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); + uploadStream = new ByteArrayInputStream(version2Bytes); + uploadedFile.uploadVersion(uploadStream); Collection versions = uploadedFile.getVersions(); BoxFileVersion previousVersion = versions.iterator().next(); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - final boolean[] onProgressChangedCalled = new boolean[]{false}; - previousVersion.download(output, new ProgressListener() { - public void onProgressChanged(long numBytes, long totalBytes) { - onProgressChangedCalled[0] = true; - - assertThat(numBytes, is(not(0L))); - assertThat(totalBytes, is(equalTo(version1Length))); - } - }); + ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); + ProgressListener mockProgressListener = mock(ProgressListener.class); + previousVersion.download(downloadStream, mockProgressListener); + String downloadedContent = downloadStream.toString(StandardCharsets.UTF_8.name()); - assertThat(onProgressChangedCalled[0], is(true)); - String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); - assertThat(downloadedFileContent, equalTo(version1Content)); + assertThat(versions, hasSize(1)); + assertThat(previousVersion.getSha1(), is(equalTo(version1Sha))); + assertThat(downloadedContent, equalTo(version1Content)); + verify(mockProgressListener).onProgressChanged(anyLong(), longThat(is(equalTo(version1Size)))); uploadedFile.delete(); - assertThat(rootFolder, not(hasItem(uploadedFile))); } @Test @Category(IntegrationTest.class) public void getInfoWithOnlyTheNameField() { - final String expectedName = "[getInfoWithOnlyTheNameField] Test File.txt"; - BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); BoxFolder rootFolder = BoxFolder.getRootFolder(api); + String fileName = "[getInfoWithOnlyTheNameField] Test File.txt"; + String fileContent = "Test file"; + byte[] fileBytes = fileContent.getBytes(StandardCharsets.UTF_8); - final String fileContent = "Test file"; - InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); - BoxFile uploadedFile = rootFolder.uploadFile(stream, expectedName, null, null); - assertThat(rootFolder, hasItem(uploadedFile)); - - BoxFile.Info info = uploadedFile.getInfo("name"); - final String actualName = info.getName(); - final String actualDescription = info.getDescription(); - final long actualSize = info.getSize(); + InputStream uploadStream = new ByteArrayInputStream(fileBytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); + BoxFile.Info uploadedFileInfo = uploadedFile.getInfo("name"); - assertThat(expectedName, equalTo(actualName)); - assertThat(actualDescription, is(nullValue())); - assertThat(actualSize, is(0L)); + assertThat(uploadedFileInfo.getName(), is(equalTo(fileName))); + assertThat(uploadedFileInfo.getDescription(), is(nullValue())); + assertThat(uploadedFileInfo.getSize(), is(equalTo(0L))); uploadedFile.delete(); - assertThat(rootFolder, not(hasItem(uploadedFile))); } @Test @Category(IntegrationTest.class) public void updateFileInfoSucceeds() { - final String originalName = "[updateFileInfoSucceeds] Original Name.txt"; - final String newName = "[updateFileInfoSucceeds] New Name.txt"; - BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); BoxFolder rootFolder = BoxFolder.getRootFolder(api); + String originalFileName = "[updateFileInfoSucceeds] Original Name.txt"; + String newFileName = "[updateFileInfoSucceeds] New Name.txt"; + String fileContent = "Test file"; + byte[] fileBytes = fileContent.getBytes(StandardCharsets.UTF_8); - final String fileContent = "Test file"; - InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); - BoxFile uploadedFile = rootFolder.uploadFile(stream, originalName, null, null); - assertThat(rootFolder, hasItem(uploadedFile)); + InputStream uploadStream = new ByteArrayInputStream(fileBytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, originalFileName); - BoxFile.Info info = uploadedFile.new Info(); - info.setName(newName); - uploadedFile.updateInfo(info); + BoxFile.Info newInfo = uploadedFile.new Info(); + newInfo.setName(newFileName); + uploadedFile.updateInfo(newInfo); + BoxFile.Info refreshedInfo = uploadedFile.getInfo(); - info = uploadedFile.getInfo(); - assertThat(info.getName(), equalTo(newName)); + assertThat(refreshedInfo.getName(), is(equalTo(newFileName))); uploadedFile.delete(); - assertThat(rootFolder, not(hasItem(uploadedFile))); - } - - @Test - @Category(IntegrationTest.class) - public void uploadMultipleVersionsSucceeds() throws UnsupportedEncodingException { - BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); - BoxFolder rootFolder = BoxFolder.getRootFolder(api); - - final String fileName = "[uploadMultipleVersionsSucceeds] Multi-version File.txt"; - final String version1Content = "Version 1"; - final String version1Sha = "db3cbc01da600701b9fe4a497fe328e71fa7022f"; - final String version2Content = "Version 2"; - - InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); - BoxFile file = rootFolder.uploadFile(stream, fileName); - assertThat(rootFolder, hasItem(file)); - - stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); - file.uploadVersion(stream); - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - file.download(output); - String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); - assertThat(downloadedFileContent, equalTo(version2Content)); - - Collection versions = file.getVersions(); - assertThat(versions, hasSize(1)); - - BoxFileVersion previousVersion = versions.iterator().next(); - assertThat(previousVersion.getSha1(), is(equalTo(version1Sha))); - - file.delete(); - assertThat(rootFolder, not(hasItem(file))); } @Test @Category(IntegrationTest.class) - public void deleteVersionDoesNotThrowException() throws UnsupportedEncodingException { + public void deleteVersionSucceeds() { BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); BoxFolder rootFolder = BoxFolder.getRootFolder(api); + String fileName = "[deleteVersionSucceeds] Multi-version File.txt"; + byte[] version1Bytes = "Version 1".getBytes(StandardCharsets.UTF_8); + byte[] version2Bytes = "Version 2".getBytes(StandardCharsets.UTF_8); - final String fileName = "[deleteVersionSucceeds] Multi-version File.txt"; - final String version1Content = "Version 1"; - final String version2Content = "Version 2"; - - InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); - BoxFile file = rootFolder.uploadFile(stream, fileName); - assertThat(rootFolder, hasItem(file)); - - stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); - file.uploadVersion(stream); - - Collection versions = file.getVersions(); - assertThat(versions, hasSize(1)); + InputStream uploadStream = new ByteArrayInputStream(version1Bytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); + uploadStream = new ByteArrayInputStream(version2Bytes); + uploadedFile.uploadVersion(uploadStream); + Collection versions = uploadedFile.getVersions(); BoxFileVersion previousVersion = versions.iterator().next(); previousVersion.delete(); - file.delete(); - assertThat(rootFolder, not(hasItem(file))); + uploadedFile.delete(); } @Test @@ -206,29 +151,26 @@ public void deleteVersionDoesNotThrowException() throws UnsupportedEncodingExcep public void promoteVersionsSucceeds() throws UnsupportedEncodingException { BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); BoxFolder rootFolder = BoxFolder.getRootFolder(api); + String fileName = "[promoteVersionsSucceeds] Multi-version File.txt"; + String version1Content = "Version 1"; + byte[] version1Bytes = version1Content.getBytes(StandardCharsets.UTF_8); + byte[] version2Bytes = "Version 2".getBytes(StandardCharsets.UTF_8); - final String fileName = "[promoteVersionsSucceeds] Multi-version File.txt"; - final String version1Content = "Version 1"; - final String version2Content = "Version 2"; - - InputStream stream = new ByteArrayInputStream(version1Content.getBytes(StandardCharsets.UTF_8)); - BoxFile file = rootFolder.uploadFile(stream, fileName); - assertThat(rootFolder, hasItem(file)); - - stream = new ByteArrayInputStream(version2Content.getBytes(StandardCharsets.UTF_8)); - file.uploadVersion(stream); + InputStream uploadStream = new ByteArrayInputStream(version1Bytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); + uploadStream = new ByteArrayInputStream(version2Bytes); + uploadedFile.uploadVersion(uploadStream); - Collection versions = file.getVersions(); + Collection versions = uploadedFile.getVersions(); BoxFileVersion previousVersion = versions.iterator().next(); previousVersion.promote(); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - file.download(output); - String downloadedFileContent = output.toString(StandardCharsets.UTF_8.name()); - assertThat(downloadedFileContent, equalTo(version1Content)); + ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); + uploadedFile.download(downloadStream); + String downloadedContent = downloadStream.toString(StandardCharsets.UTF_8.name()); + assertThat(downloadedContent, equalTo(version1Content)); - file.delete(); - assertThat(rootFolder, not(hasItem(file))); + uploadedFile.delete(); } @Test @@ -236,22 +178,23 @@ public void promoteVersionsSucceeds() throws UnsupportedEncodingException { public void copyFileSucceeds() throws UnsupportedEncodingException { BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); BoxFolder rootFolder = BoxFolder.getRootFolder(api); + String originalFileName = "[copyFileSucceeds] Original File.txt"; + String newFileName = "[copyFileSucceeds] New File.txt"; + String fileContent = "Test file"; + byte[] fileBytes = fileContent.getBytes(StandardCharsets.UTF_8); - final String originalName = "[copyFileSucceeds] Original File.txt"; - final String newName = "[copyFileSucceeds] New File.txt"; - final String fileContent = "Test file"; + InputStream uploadStream = new ByteArrayInputStream(fileBytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, originalFileName); - InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); - BoxFile uploadedFile = rootFolder.uploadFile(stream, originalName); - assertThat(rootFolder, hasItem(uploadedFile)); - - BoxFile.Info copiedFileInfo = uploadedFile.copy(rootFolder, newName); + BoxFile.Info copiedFileInfo = uploadedFile.copy(rootFolder, newFileName); BoxFile copiedFile = copiedFileInfo.getResource(); - assertThat(rootFolder, hasItem(copiedFile)); + + ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); + copiedFile.download(downloadStream); + String downloadedContent = downloadStream.toString(StandardCharsets.UTF_8.name()); + assertThat(downloadedContent, equalTo(fileContent)); uploadedFile.delete(); - assertThat(rootFolder, not(hasItem(uploadedFile))); copiedFile.delete(); - assertThat(rootFolder, not(hasItem(copiedFile))); } } From 8861fe3437b807e065c7a34514a9f4d41f325b75 Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Thu, 25 Sep 2014 16:07:00 -0700 Subject: [PATCH 37/39] Change addPendingChange to take a JsonValue This makes it possible to update nested objects within an Info object. For example, when updating the shared link JSON object within a file info JSON object. --- src/main/java/com/box/sdk/BoxItem.java | 4 ++-- src/main/java/com/box/sdk/BoxResource.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxItem.java b/src/main/java/com/box/sdk/BoxItem.java index f613b2687..6a096522e 100644 --- a/src/main/java/com/box/sdk/BoxItem.java +++ b/src/main/java/com/box/sdk/BoxItem.java @@ -55,7 +55,7 @@ public String getName() { public void setName(String name) { this.name = name; - this.addPendingChange("name", name); + this.addPendingChange("name", JsonValue.valueOf(name)); } public Date getCreatedAt() { @@ -72,7 +72,7 @@ public String getDescription() { public void setDescription(String description) { this.description = description; - this.addPendingChange("description", description); + this.addPendingChange("description", JsonValue.valueOf(description)); } public long getSize() { diff --git a/src/main/java/com/box/sdk/BoxResource.java b/src/main/java/com/box/sdk/BoxResource.java index 54a7b32e2..6fd51805d 100644 --- a/src/main/java/com/box/sdk/BoxResource.java +++ b/src/main/java/com/box/sdk/BoxResource.java @@ -3,6 +3,7 @@ import java.util.List; import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; /** * The abstract base class for all resource types (files, folders, comments, collaborations, etc.) used by the API. @@ -140,7 +141,7 @@ public String getPendingChanges() { * @param key the name of the field. * @param value the new value of the field. */ - protected void addPendingChange(String key, String value) { + protected void addPendingChange(String key, JsonValue value) { this.pendingChanges.set(key, value); } From 988d443387d55139e09b582aba36730352d46d4f Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Fri, 26 Sep 2014 15:14:22 -0700 Subject: [PATCH 38/39] Add BoxJSONObject class This class helps encoding and decoding JSON. It also tracks local changes to an object until they can be sent back to the Box API. --- src/main/java/com/box/sdk/BoxJSONObject.java | 163 +++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/main/java/com/box/sdk/BoxJSONObject.java diff --git a/src/main/java/com/box/sdk/BoxJSONObject.java b/src/main/java/com/box/sdk/BoxJSONObject.java new file mode 100644 index 000000000..b1e3a9669 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxJSONObject.java @@ -0,0 +1,163 @@ +package com.box.sdk; + +import java.util.HashMap; +import java.util.Map; + +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +/** + * The abstract base class for all types that contain JSON data returned by the Box API. The most common implementation + * of BoxJSONObject is {@link BoxResource.Info} and its subclasses. Changes made to a BoxJSONObject will be tracked + * locally until the pending changes are sent back to Box in order to avoid unnecessary network requests. + * + */ +public abstract class BoxJSONObject { + /** + * The JsonObject that contains any local pending changes. When getPendingChanges is called, this object will be + * encoded to a JSON string. + */ + private JsonObject pendingChanges; + + /** + * A map of other BoxJSONObjects which will be lazily converted to a JsonObject once getPendingChanges is called. + * This allows changes to be made to a child BoxJSONObject and still have those changes reflected in the JSON + * string. + */ + private Map lazyPendingChanges; + + /** + * Constructs an empty BoxJSONObject. + */ + public BoxJSONObject() { + this.lazyPendingChanges = new HashMap(); + } + + /** + * Constructs a BoxJSONObject by decoding it from a JSON string. + * @param json the JSON string to decode. + */ + public BoxJSONObject(String json) { + this(JsonObject.readFrom(json)); + } + + /** + * Constructs a BoxJSONObject using an already parsed JSON object. + * @param jsonObject the parsed JSON object. + */ + BoxJSONObject(JsonObject jsonObject) { + this(); + + this.update(jsonObject); + } + + /** + * Clears any pending changes from this JSON object. + */ + public void clearPendingChanges() { + this.pendingChanges = null; + this.lazyPendingChanges.clear(); + } + + /** + * Gets a JSON string containing any pending changes to this object that can be sent back to the Box API. + * @return a JSON string containing the pending changes. + */ + public String getPendingChanges() { + JsonObject jsonObject = this.getPendingJSONObject(); + if (jsonObject == null) { + return null; + } + + return jsonObject.toString(); + } + + /** + * Invoked with a JSON member whenever this object is updated or created from a JSON object. + * + *

Subclasses should override this method in order to parse any JSON members it knows about. This method is a + * no-op by default.

+ * + * @param member the JSON member to be parsed. + */ + void parseJSONMember(JsonObject.Member member) { } + + /** + * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next + * time {@link #getPendingChanges} is called. + * @param key the name of the field. + * @param value the new boolean value of the field. + */ + void addPendingChange(String key, boolean value) { + if (this.pendingChanges == null) { + this.pendingChanges = new JsonObject(); + } + + this.pendingChanges.set(key, value); + } + + /** + * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next + * time {@link #getPendingChanges} is called. + * @param key the name of the field. + * @param value the new String value of the field. + */ + void addPendingChange(String key, String value) { + this.addPendingChange(key, JsonValue.valueOf(value)); + } + + /** + * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next + * time {@link #getPendingChanges} is called. + * @param key the name of the field. + * @param value the new BoxJSONObject value of the field. + */ + void addPendingChange(String key, BoxJSONObject value) { + this.lazyPendingChanges.put(key, value); + } + + /** + * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next + * time {@link #getPendingChanges} is called. + * @param key the name of the field. + * @param value the JsonValue of the field. + */ + private void addPendingChange(String key, JsonValue value) { + if (this.pendingChanges == null) { + this.pendingChanges = new JsonObject(); + } + + this.pendingChanges.set(key, value); + } + + /** + * Updates this BoxJSONObject using the information in a JSON object. + * @param jsonObject the JSON object containing updated information. + */ + void update(JsonObject jsonObject) { + for (JsonObject.Member member : jsonObject) { + if (member.getValue().isNull()) { + continue; + } + + this.parseJSONMember(member); + } + + this.clearPendingChanges(); + } + + /** + * Gets a JsonObject containing any pending changes to this object that can be sent back to the Box API. + * @return a JsonObject containing the pending changes. + */ + JsonObject getPendingJSONObject() { + if (this.pendingChanges == null && !this.lazyPendingChanges.isEmpty()) { + this.pendingChanges = new JsonObject(); + } + + for (Map.Entry entry : this.lazyPendingChanges.entrySet()) { + this.pendingChanges.set(entry.getKey(), entry.getValue().getPendingJSONObject()); + } + return this.pendingChanges; + } +} From 6c75538887b1dca0f4e3af2cf476fa17a3d6fbde Mon Sep 17 00:00:00 2001 From: Greg Curtis Date: Fri, 26 Sep 2014 15:20:30 -0700 Subject: [PATCH 39/39] Make BoxResource.Info a subclass of BoxJSONObject --- src/main/java/com/box/sdk/BoxFile.java | 2 +- src/main/java/com/box/sdk/BoxFolder.java | 2 +- src/main/java/com/box/sdk/BoxItem.java | 4 +- src/main/java/com/box/sdk/BoxResource.java | 72 ++-------------------- 4 files changed, 8 insertions(+), 72 deletions(-) diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 1438e7323..71aa60b47 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -109,7 +109,7 @@ public void updateInfo(BoxFile.Info info) { request.setBody(info.getPendingChanges()); BoxJSONResponse response = (BoxJSONResponse) request.send(); JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); - info.updateFromJSON(jsonObject); + info.update(jsonObject); } public Collection getVersions() { diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index 05c3845c7..b742c59e9 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -48,7 +48,7 @@ public void updateInfo(BoxFolder.Info info) { request.setBody(info.getPendingChanges()); BoxJSONResponse response = (BoxJSONResponse) request.send(); JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); - info.updateFromJSON(jsonObject); + info.update(jsonObject); } public BoxFolder.Info copy(BoxFolder destination) { diff --git a/src/main/java/com/box/sdk/BoxItem.java b/src/main/java/com/box/sdk/BoxItem.java index 6a096522e..f613b2687 100644 --- a/src/main/java/com/box/sdk/BoxItem.java +++ b/src/main/java/com/box/sdk/BoxItem.java @@ -55,7 +55,7 @@ public String getName() { public void setName(String name) { this.name = name; - this.addPendingChange("name", JsonValue.valueOf(name)); + this.addPendingChange("name", name); } public Date getCreatedAt() { @@ -72,7 +72,7 @@ public String getDescription() { public void setDescription(String description) { this.description = description; - this.addPendingChange("description", JsonValue.valueOf(description)); + this.addPendingChange("description", description); } public long getSize() { diff --git a/src/main/java/com/box/sdk/BoxResource.java b/src/main/java/com/box/sdk/BoxResource.java index 6fd51805d..759a5b4a4 100644 --- a/src/main/java/com/box/sdk/BoxResource.java +++ b/src/main/java/com/box/sdk/BoxResource.java @@ -1,9 +1,6 @@ package com.box.sdk; -import java.util.List; - import com.eclipsesource.json.JsonObject; -import com.eclipsesource.json.JsonValue; /** * The abstract base class for all resource types (files, folders, comments, collaborations, etc.) used by the API. @@ -79,14 +76,12 @@ public int hashCode() { * * @param the type of the resource associated with this info. */ - public abstract class Info { - private JsonObject pendingChanges; - + public abstract class Info extends BoxJSONObject { /** * Constructs an empty Info object. */ public Info() { - this.pendingChanges = new JsonObject(); + super(); } /** @@ -94,7 +89,7 @@ public Info() { * @param json the JSON string to parse. */ public Info(String json) { - this(JsonObject.readFrom(json)); + super(json); } /** @@ -102,7 +97,7 @@ public Info(String json) { * @param jsonObject the parsed JSON object. */ protected Info(JsonObject jsonObject) { - this.updateFromJSON(jsonObject); + super(jsonObject); } /** @@ -113,69 +108,10 @@ public String getID() { return BoxResource.this.getID(); } - /** - * Gets a list of fields that have pending changes that haven't been sent to the API yet. - * @return a list of changed fields with pending changes. - */ - public List getChangedFields() { - return this.pendingChanges.names(); - } - - /** - * Gets a JSON object of any pending changes. - * @return a JSON object containing the pending changes. - */ - public String getPendingChanges() { - return this.pendingChanges.toString(); - } - /** * Gets the resource associated with this Info. * @return the associated resource. */ public abstract T getResource(); - - /** - * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next - * time {@link #getPendingChanges} is called. - * @param key the name of the field. - * @param value the new value of the field. - */ - protected void addPendingChange(String key, JsonValue value) { - this.pendingChanges.set(key, value); - } - - /** - * Clears all pending changes. - */ - protected void clearPendingChanges() { - this.pendingChanges = new JsonObject(); - } - - /** - * Updates this Info object using the information in a JSON object. - * @param jsonObject the JSON object containing updated information. - */ - protected void updateFromJSON(JsonObject jsonObject) { - for (JsonObject.Member member : jsonObject) { - if (member.getValue().isNull()) { - continue; - } - - this.parseJSONMember(member); - } - - this.clearPendingChanges(); - } - - /** - * Invoked with a JSON member whenever this Info object is updated or created from a JSON object. - * - *

Subclasses should override this method in order to parse any JSON members it knows about. This method is a - * no-op by default.

- * - * @param member the JSON member to be parsed. - */ - protected void parseJSONMember(JsonObject.Member member) { } } }