diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fc1d7822f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,108 @@ +Contributing +============ + +All contributions are welcome to this project. + +Contributor License Agreement +----------------------------- + +Before a contribution can be merged into this project, please fill out the +Contributor License Agreement (CLA) located at: + +http://opensource.box.com/cla + +To learn more about CLAs and why they are important to open source projects, +please see the [Wikipedia entry][1]. + +How to contribute +----------------- + +* **File an issue** - if you found a bug, want to request an enhancement, or + want to implement something (bug fix or feature). +* **Send a pull request** - if you want to contribute code. Please be sure to + file an issue first. + +Pull request best practices +--------------------------- + +Following these steps will help ensure that your pull request gets reviewed and +accepted as quickly as possible. + +### Step 1: File an issue + +Before writing any code, please file an issue stating the problem you want to +solve or the feature you want to implement. This allows us to give you feedback +before you spend any time writing code. There may be a known limitation that +can't be addressed, or a bug that has already been fixed in a different way. The +issue allows us to communicate and figure out if it's worth your time to write a +bunch of code for the project. + +### Step 2: Fork this repository in GitHub + +This will create your own copy of our repository. + +### Step 3: Add the upstream source + +The upstream source is the project under the Box organization on GitHub. To add +an upstream source for this project, type: + +``` +git remote add upstream git@github.com:box/box-java-sdk.git +``` + +This will come in useful later. + +### Step 4: Create a feature branch + +Create a branch with a descriptive name, such as `add-search`. + +### Step 5: Push your feature branch to your fork + +As you develop code, continue to push code to your remote feature branch. Please +make sure to include the issue number you're addressing in the body of your +commit message and adhere to [standard git commit message guidelines][2]. For +example: + +``` +Add search + +Introduced a new BoxSearch class that uses the /search API endpoint. + +Closes #123. +``` + +This helps us out by allowing us to track which issue your commit relates to. + +Keep a separate feature branch for each issue you want to address. + +### Step 6: Rebase + +Before sending a pull request, rebase against upstream, such as: + +``` +git fetch upstream +git rebase upstream/master +``` + +This will add your changes on top of what's already in upstream, minimizing +merge issues. + +### Step 7: Build and test your changes + +Make sure that the code builds and passes all unit tests by running: + +```bash +$ gradle clean build +``` + +### Step 8: Send the pull request + +Send the pull request from your feature branch to us. Be sure to include a +description in your commit message (not the pull request description) that lets +us know what work you did. + +Keep in mind that we like to see one issue addressed per pull request, as this +helps keep our git history clean and we can more easily track down issues. + +[1]: http://en.wikipedia.org/wiki/Contributor_License_Agreement +[2]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html diff --git a/build.gradle b/build.gradle index 601d2712d..92745b5d9 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,19 @@ dependencies { testCompile 'org.slf4j:slf4j-nop:1.7.7' } -tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:all' +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/' } task javadocJar(type: Jar) { @@ -32,8 +43,19 @@ task sourcesJar(type: Jar) { from sourceSets.main.allSource } -artifacts { - archives sourcesJar, javadocJar +task integrationTest(type: Test) { + description 'Runs the integration tests.' + group 'Verification' + + useJUnit { + includeCategories 'com.box.sdk.IntegrationTest' + } +} + +jacocoTestReport.dependsOn(integrationTest); + +tasks.withType(JavaCompile) { + options.compilerArgs << '-Xlint:all' } tasks.withType(Test) { @@ -48,32 +70,12 @@ tasks.withType(Test) { } } -test { - useJUnit { - excludeCategories 'com.box.sdk.IntegrationTest' - } +artifacts { + archives sourcesJar, javadocJar } -task integrationTest(type: Test) { - description 'Runs the integration tests.' - group 'Verification' - +test { useJUnit { - includeCategories 'com.box.sdk.IntegrationTest' + excludeCategories 'com.box.sdk.IntegrationTest' } } - -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 index 55f07a07a..1e10ab4be 100644 --- a/doc/css/javadoc.css +++ b/doc/css/javadoc.css @@ -217,7 +217,6 @@ div.summary ul.blockList li.blockList ul.blockList li.blockList ul.blockList li. position: relative; } .indexContainer { - background-color: rgb(64, 64, 64); margin: 10px; position: relative; font-size: 1.0em; diff --git a/doc/overview.md b/doc/overview.md index 5bbacca70..35793a282 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -5,8 +5,8 @@ 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 ---------------- +Authentication +-------------- 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 @@ -24,28 +24,6 @@ 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`. -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. - -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: - -```java -BoxAPIConnection api = new BoxAPIConnection("token"); -URL url = new URL("https://api.box.com/2.0/folders/0/items?fields=name,created_at") -BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); -BoxJSONResponse response = (BoxJSONResponse) request.send(); -String json = response.getJSON(); -``` - Resource Types -------------- @@ -72,3 +50,29 @@ BoxFolder.Info info = folder.getInfo(); // This BoxUser has the same BoxAPIConnection as "folder". BoxUser creator = info.getCreatedBy(); ``` + +### Resource Docs + +* [Files](types/files.md) + +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. + +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: + +```java +BoxAPIConnection api = new BoxAPIConnection("token"); +URL url = new URL("https://api.box.com/2.0/folders/0/items?fields=name,created_at") +BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); +BoxJSONResponse response = (BoxJSONResponse) request.send(); +String json = response.getJSON(); +``` diff --git a/doc/resource-types.md b/doc/resource-types.md deleted file mode 100644 index c7cd908d4..000000000 --- a/doc/resource-types.md +++ /dev/null @@ -1,83 +0,0 @@ -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(); -``` diff --git a/doc/types/files.md b/doc/types/files.md new file mode 100644 index 000000000..7a0f02137 --- /dev/null +++ b/doc/types/files.md @@ -0,0 +1,186 @@ +Files +===== + +File objects represent individual files in Box. They can be used to download a +file's contents, upload new versions, and perform other common file operations +(move, copy, delete, etc.). + +* [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 +------------------------ + +Calling `getInfo()` on a file returns a snapshot of the file's info. + +```java +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info info = file.getInfo(); +``` + +Requesting information for only the fields you need can improve performance and +reduce the size of the network request. + +```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 +--------------------------- + +Updating a file's information is done by creating a new `BoxFile.Info` object or +updating an existing one, and then calling `updateInfo(BoxFile.Info)`. + +```java +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info info = file.new Info(); +info.setName("New Name"); +file.updateInfo(info); +``` + +Download a File +--------------- + +A file can be downloaded by calling `download(OutputStream)` and providing an +`OutputStream` where the file's contents will be written. + +```java +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info info = file.getInfo(); + +FileOutputStream stream = new FileOutputStream(info.getName()); +file.download(stream); +stream.close(); +``` + +Download progress can be tracked by providing a `ProgressListener` which will +receive progress updates as the download completes. + +```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 +------------- + +Files are uploaded to a folder by calling the `uploadFile(InputStream, String)` +method. + +```java +BoxFolder rootFolder = BoxFolder.getRootFolder(api); +FileInputStream stream = new FileInputStream("My File.txt"); +rootFolder.uploadFile(stream, "My File.txt"); +stream.close(); +``` + +Upload progress can be tracked by providing the size of the file and a +`ProgressListener`. The `ProgressListener` will receive progress updates as the +upload completes. + +```java +BoxFolder rootFolder = BoxFolder.getRootFolder(api); +FileInputStream stream = new FileInputStream("My File.txt"); +rootFolder.uploadFile(stream, "My File.txt", 1024, new ProgressListener() { + public void onProgressChanged(long numBytes, long totalBytes) { + double percentComplete = numBytes / totalBytes; + } +}); +stream.close(); +``` + +Copy a File +----------- + +A file can be copied to a new folder and optionally be renamed. + +```java +BoxFolder rootFolder = BoxFolder.getRootFolder(api); +BoxFile file = new BoxFile(api, "id"); +BoxFile.Info copiedFileInfo = file.copy(rootFolder, "New Name"); +``` + +Delete a File +------------- + +Calling the `delete()` method will move the file to the user's trash. + +```java +BoxFile file = new BoxFile(api, "id"); +file.delete(); +``` + +Get Previous Versions of a File +------------------------------- + +For users with premium accounts, versions of a file can be retrieved. + +```java +BoxFile file = new BoxFile(api, "id"); +List versions = file.getVersions(); +for (BoxFileVersion version : versions) { + System.out.format("SHA1 of \"%s\": %s\n", item.getName(), version.getSha1()); +} +``` + +Upload a New Version of a File +------------------------------ + +For users with premium accounts, new versions of a file can be uploaded. + +```java +BoxFile file = new BoxFile(api, "id"); +FileInputStream stream = new FileInputStream("My File.txt"); +file.uploadVersion(stream); +``` + +Download a Previous Version of a File +------------------------------------- + +For users with premium accounts, previous versions of a file can be downloaded. + +```java +BoxFile file = new BoxFile(api, "id"); +List versions = file.getVersions(); +BoxFileVersion firstVersion = versions.get(0); + +FileOutputStream stream = new FileOutputStream(firstVersion.getName()); +firstVersion.download(stream); +stream.close(); +``` + +Promote a Previous Version of a File +------------------------------------ + +A previous version of a file can be promoted to become the current version of +the file. + +```java +BoxFile file = new BoxFile(api, "id"); +List versions = file.getVersions(); +BoxFileVersion firstVersion = versions.get(0); +firstVersion.promote(); +``` + +Delete a Previous Version of a File +----------------------------------- + +A version of a file can be deleted and moved to the trash. + +```java +BoxFile file = new BoxFile(api, "id"); +List versions = file.getVersions(); +BoxFileVersion firstVersion = versions.get(0); +firstVersion.delete(); +``` diff --git a/doc/upgrading.md b/doc/upgrading.md new file mode 100644 index 000000000..9a3a0d45f --- /dev/null +++ b/doc/upgrading.md @@ -0,0 +1,116 @@ +Upgrading to v4 +=============== + +v4 of the Box Java SDK introduces a lot of new design changes. Since this is a +major version bump, there will be breaking changes that will require you to +update your application. + +What's New +---------- + +### Features + +* __Automatic rate-limiting and error retry.__ API requests will automatically + be retried with exponential back off if a 500+ (server error) or 429 (too many + requests) response code is returned. +* __OAuth redesign.__ OAuth should now be easier to use, allowing you to + authenticate with an access token, auth code, or developer token. +* __New EventStream class.__ This class makes it easier to listen for API events + by allowing you to specify listeners that will be notified when an event + occurs. +* __New classes for making custom API requests.__ The BoxAPIRequest and + BoxAPIResponse classes make it easy to send custom requests to the API while + still having OAuth, rate-limiting back off, error handling and response + parsing automatically handled. + +### General Improvements + +* __Simpler and more intuitive design.__ We aimed to make the overall design of + the SDK more intuitive and easier to learn. +* __More documentation and examples.__ The Javadocs have been completely + overhauled and there are new guides explaining how to accomplish common tasks + with the SDK. +* __SDK size has been dramatically decreased.__ Many of the SDK's dependencies + have been removed and its overall size has been reduced - making it more + suitable for mobile apps. +* __Easier integration.__ With a single build process, it's easier and simpler + to get the SDK building with other applications. It also follows the standard + directory layout, making it easier to import into various IDEs or build + systems. + +Authentication +-------------- + +Authentication has been simplified by allowing you to provide tokens or auth +codes directly. All authentication is now done by creating a `BoxAPIConnection` +in order to establish an authenticated connection with the API. + +Connect to the API using a developer token: + +```java +BoxAPIConnection api = new BoxAPIConnection("YOUR-DEVELOPER-TOKEN"); +``` + +Connect to the API using access and refresh tokens: + +```java +BoxAPIConnection api = new BoxAPIConnection("CLIENT-ID", "CLIENT-SECRET", + "ACCESS-TOKEN", "REFRESH-TOKEN"); +``` + +Connect to the API using an auth code: + +```java +BoxAPIConnection api = new BoxAPIConnection("CLIENT-ID", "CLIENT-SECRET", + "AUTH-CODE"); +``` + +More information on authentication can be found [here](authentication.md). + +New Resource Types +------------------ + +Previously, interaction with the API was done through managers and request +objects. For example: + +```java +BoxClient client = new BoxClient(...); +IBoxFilesManager filesManager = boxClient.getFilesManager(); + +BoxDefaultRequestObject requestObj = new BoxDefaultRequestObject(); +requestObj.getRequestExtras().addField(BoxFile.FIELD_SHA1); +requestObj.getRequestExtras().addField(BoxFile.FIELD_DESCRIPTION); + +BoxFile file = filesManager.getFile(fileId, requestObj); +``` + +These objects have been removed and interacting with the API has been +simplified. Resources can now be manipulated directly without needing to use +managers or build custom requests. + +```java +BoxAPIConnection api = new BoxAPIConnection(...); +BoxFile file = new BoxFile(api, fileID); +BoxFile.Info fileInfo = file.getInfo("sha1", "description"); +``` + +More information on resource types can be found [here](resource-types.md). + +Custom Requests +--------------- + +The SDK now provides request and response objects that allow you to easily make +custom requests to the Box API. These objects will handle authentication, +automatic retry, rate-limiting and errors out-of-the-box, giving you the +flexibility to make your own API requests without having to worry about handling +these things yourself. + +```java +BoxAPIConnection api = new BoxAPIConnection(...); +URL url = new URL("https://api.box.com/2.0/folders/0/items?fields=name,created_at") +BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); +BoxJSONResponse response = (BoxJSONResponse) request.send(); +String json = response.getJSON(); +``` + +More information on resource types can be found [here](overview.md). diff --git a/src/main/java/com/box/sdk/BoxAPIRequest.java b/src/main/java/com/box/sdk/BoxAPIRequest.java index fdc337178..e5121eb08 100644 --- a/src/main/java/com/box/sdk/BoxAPIRequest.java +++ b/src/main/java/com/box/sdk/BoxAPIRequest.java @@ -92,7 +92,7 @@ public void setTimeout(int timeout) { * 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}. + * 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. */ @@ -100,6 +100,22 @@ public void setBody(InputStream stream) { this.body = stream; } + /** + * Sets the request body to the contents of an InputStream. + * + *

Providing the length of the InputStream allows for the progress of the request to be monitored when calling + * {@link #send(ProgressListener)}.

+ * + *

See {@link #setBody(InputStream)} for more information on setting the body of the request.

+ * + * @param stream an InputStream containing the contents of the body. + * @param length the expected length of the stream. + */ + public void setBody(InputStream stream, long length) { + this.bodyLength = length; + this.body = stream; + } + /** * Sets the request body to the contents of a String. * @@ -130,6 +146,23 @@ public void setBody(String body) { * @return a {@link BoxAPIResponse} containing the server's response. */ public BoxAPIResponse send() { + return this.send(null); + } + + /** + * Sends this request while monitoring its progress and returns a BoxAPIResponse containing the server's response. + * + *

A ProgressListener is generally only useful when the size of the request is known beforehand. If the size is + * unknown, then the ProgressListener will be updated for each byte sent, but the total number of bytes will be + * reported as 0.

+ * + *

See {@link #send} for more information on sending requests.

+ * + * @param listener a listener for monitoring the progress of the request. + * @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(ProgressListener listener) { if (this.api == null) { this.backoffCounter.reset(BoxAPIConnection.DEFAULT_MAX_ATTEMPTS); } else { @@ -138,7 +171,7 @@ public BoxAPIResponse send() { while (this.backoffCounter.getAttemptsRemaining() > 0) { try { - return this.trySend(); + return this.trySend(listener); } catch (BoxAPIException apiException) { if (!this.backoffCounter.decrement() || !isResponseRetryable(apiException.getResponseCode())) { throw apiException; @@ -208,10 +241,6 @@ public String toString() { return builder.toString().trim(); } - 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. @@ -233,7 +262,7 @@ protected String bodyToString() { * @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) { + protected void writeBody(HttpURLConnection connection, ProgressListener listener) { if (this.body == null) { return; } @@ -241,6 +270,9 @@ protected void writeBody(HttpURLConnection connection) { connection.setDoOutput(true); try { OutputStream output = connection.getOutputStream(); + if (listener != null) { + output = new ProgressOutputStream(output, listener, this.bodyLength); + } int b = this.body.read(); while (b != -1) { output.write(b); @@ -266,7 +298,11 @@ protected void resetBody() throws IOException { } } - private BoxAPIResponse trySend() { + void setBackoffCounter(BackoffCounter counter) { + this.backoffCounter = counter; + } + + private BoxAPIResponse trySend(ProgressListener listener) { HttpURLConnection connection = this.createConnection(); if (this.bodyLength > 0) { connection.setFixedLengthStreamingMode(this.bodyLength); @@ -278,7 +314,7 @@ private BoxAPIResponse trySend() { } this.requestProperties = connection.getRequestProperties(); - this.writeBody(connection); + this.writeBody(connection, listener); // Ensure that we're connected in case writeBody() didn't write anything. try { diff --git a/src/main/java/com/box/sdk/BoxAPIResponse.java b/src/main/java/com/box/sdk/BoxAPIResponse.java index 1c8cc3746..7661e5480 100644 --- a/src/main/java/com/box/sdk/BoxAPIResponse.java +++ b/src/main/java/com/box/sdk/BoxAPIResponse.java @@ -61,6 +61,7 @@ public InputStream getBody() { throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); } } + return this.inputStream; } diff --git a/src/main/java/com/box/sdk/BoxCollaboration.java b/src/main/java/com/box/sdk/BoxCollaboration.java new file mode 100644 index 000000000..02191e7d4 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxCollaboration.java @@ -0,0 +1,242 @@ +package com.box.sdk; + +import java.net.URL; +import java.text.ParseException; +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 BoxCollaboration extends BoxResource { + private static final URLTemplate COLLABORATIONS_URL_TEMPLATE = new URLTemplate("collaborations"); + private static final URLTemplate PENDING_COLLABORATIONS_URL = new URLTemplate("collaborations?status=pending"); + private static final URLTemplate COLLABORATION_URL_TEMPLATE = new URLTemplate("collaborations/%s"); + + public BoxCollaboration(BoxAPIConnection api, String id) { + super(api, id); + } + + public static Collection getPendingCollaborations(BoxAPIConnection api) { + URL url = PENDING_COLLABORATIONS_URL.build(api.getBaseURL()); + + BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); + + int entriesCount = responseJSON.get("total_count").asInt(); + Collection collaborations = new ArrayList(entriesCount); + JsonArray entries = responseJSON.get("entries").asArray(); + for (JsonValue entry : entries) { + JsonObject entryObject = entry.asObject(); + BoxCollaboration collaboration = new BoxCollaboration(api, entryObject.get("id").asString()); + BoxCollaboration.Info info = collaboration.new Info(entryObject); + collaborations.add(info); + } + + return collaborations; + } + + public Info getInfo() { + BoxAPIConnection api = this.getAPI(); + URL url = COLLABORATIONS_URL_TEMPLATE.build(api.getBaseURL()); + + BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); + return new Info(jsonObject); + } + + public void updateInfo(Info info) { + BoxAPIConnection api = this.getAPI(); + URL url = COLLABORATION_URL_TEMPLATE.build(api.getBaseURL(), this.getID()); + + BoxJSONRequest request = new BoxJSONRequest(api, url, "PUT"); + request.setBody(info.getPendingChanges()); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); + info.update(jsonObject); + } + + public void delete() { + BoxAPIConnection api = this.getAPI(); + URL url = COLLABORATION_URL_TEMPLATE.build(api.getBaseURL(), this.getID()); + + BoxAPIRequest request = new BoxAPIRequest(api, url, "DELETE"); + BoxAPIResponse response = request.send(); + response.disconnect(); + } + + public class Info extends BoxResource.Info { + private BoxUser.Info createdBy; + private Date createdAt; + private Date modifiedAt; + private Date expiresAt; + private Status status; + private BoxCollaborator.Info accessibleBy; + private Role role; + private Date acknowledgedAt; + private BoxFolder.Info item; + + Info(JsonObject jsonObject) { + super(jsonObject); + } + + public BoxUser.Info getCreatedBy() { + return this.createdBy; + } + + public Date getCreatedAt() { + return this.createdAt; + } + + public Date getModifiedAt() { + return this.modifiedAt; + } + + public Date getExpiresAt() { + return this.expiresAt; + } + + public Status getStatus() { + return this.status; + } + + public void setStatus(Status status) { + this.status = status; + this.addPendingChange("status", status.name().toLowerCase()); + } + + public BoxCollaborator.Info getAccessibleBy() { + return this.accessibleBy; + } + + public Role getRole() { + return this.role; + } + + public void setRole(Role role) { + this.role = role; + this.addPendingChange("role", role.toJSONString()); + } + + public Date getAcknowledgedAt() { + return this.acknowledgedAt; + } + + public BoxFolder.Info getItem() { + return this.item; + } + + @Override + public BoxCollaboration getResource() { + return BoxCollaboration.this; + } + + @Override + protected void parseJSONMember(JsonObject.Member member) { + super.parseJSONMember(member); + + String memberName = member.getName(); + JsonValue value = member.getValue(); + try { + switch (memberName) { + case "created_by": + JsonObject userJSON = value.asObject(); + String userID = userJSON.get("id").asString(); + BoxUser user = new BoxUser(getAPI(), userID); + this.createdBy = user.new Info(userJSON); + break; + case "created_at": + this.createdAt = BoxDateParser.parse(value.asString()); + break; + case "modified_at": + this.modifiedAt = BoxDateParser.parse(value.asString()); + break; + case "expires_at": + this.expiresAt = BoxDateParser.parse(value.asString()); + break; + case "status": + String statusString = value.asString().toUpperCase(); + this.status = Status.valueOf(statusString); + break; + case "accessible_by": + userJSON = value.asObject(); + userID = userJSON.get("id").asString(); + user = new BoxUser(getAPI(), userID); + BoxUser.Info userInfo = user.new Info(userJSON); + this.accessibleBy = userInfo; + break; + case "role": + this.role = Role.fromJSONString(value.asString()); + break; + case "acknowledged_at": + this.acknowledgedAt = BoxDateParser.parse(value.asString()); + break; + case "item": + JsonObject folderJSON = value.asObject(); + String folderID = folderJSON.get("id").asString(); + BoxFolder folder = new BoxFolder(getAPI(), folderID); + this.item = folder.new Info(folderJSON); + break; + default: + break; + } + } catch (ParseException e) { + assert false : "A ParseException indicates a bug in the SDK."; + } + } + } + + public enum Status { + ACCEPTED, + PENDING, + REJECTED; + } + + public enum Role { + EDITOR ("editor"), + VIEWER ("viewer"), + PREVIEWER ("previewer"), + UPLOADER ("uploader"), + PREVIEWER_UPLOADER ("previewer uploader"), + VIEWER_UPLOADER ("viewer uploader"), + CO_OWNER ("co-owner"), + OWNER ("owner"); + + private final String jsonValue; + + private Role(String jsonValue) { + this.jsonValue = jsonValue; + } + + public static Role fromJSONString(String jsonValue) { + switch (jsonValue) { + case "editor": + return EDITOR; + case "viewer": + return VIEWER; + case "previewer": + return PREVIEWER; + case "uploader": + return UPLOADER; + case "previewer uploader": + return PREVIEWER_UPLOADER; + case "viewer uploader": + return VIEWER_UPLOADER; + case "co-owner": + return CO_OWNER; + case "owner": + return OWNER; + default: + throw new IllegalArgumentException("The provided JSON value isn't a valid Role."); + } + } + + public String toJSONString() { + return this.jsonValue; + } + } +} diff --git a/src/main/java/com/box/sdk/BoxCollaborator.java b/src/main/java/com/box/sdk/BoxCollaborator.java new file mode 100644 index 000000000..b2ee178b6 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxCollaborator.java @@ -0,0 +1,111 @@ +package com.box.sdk; + +import java.text.ParseException; +import java.util.Date; + +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +/** + * The abstract base class for types that can be added to collaborations. + */ +public abstract class BoxCollaborator extends BoxResource { + + /** + * Constructs a BoxCollaborator for a collaborator with a given ID. + * @param api the API connection to be used by the collaborator. + * @param id the ID of the collaborator. + */ + public BoxCollaborator(BoxAPIConnection api, String id) { + super(api, id); + } + + /** + * Contains additional information about a BoxCollaborator. + */ + public abstract class Info extends BoxResource.Info { + private String name; + private Date createdAt; + private Date modifiedAt; + + /** + * Constructs an empty Info object. + */ + public Info() { + super(); + } + + /** + * Constructs an Info object by parsing information from a JSON string. + * @param json the JSON string to parse. + */ + public Info(String json) { + super(json); + } + + /** + * Constructs an Info object using an already parsed JSON object. + * @param jsonObject the parsed JSON object. + */ + protected Info(JsonObject jsonObject) { + super(jsonObject); + } + + /** + * Gets the name of the collaborator. + * @return the name of the collaborator. + */ + public String getName() { + return this.name; + } + + /** + * Sets the name of the collaborator. + * @param name the new name of the collaborator. + */ + public void setName(String name) { + this.name = name; + this.addPendingChange("name", name); + } + + /** + * Gets the date that the collaborator was created. + * @return the date that the collaborator was created. + */ + public Date getCreatedAt() { + return this.createdAt; + } + + /** + * Gets the date that the collaborator was modified. + * @return the date that the collaborator was modified. + */ + public Date getModifiedAt() { + return this.modifiedAt; + } + + @Override + protected void parseJSONMember(JsonObject.Member member) { + super.parseJSONMember(member); + + try { + JsonValue value = member.getValue(); + switch (member.getName()) { + case "name": + this.name = value.asString(); + break; + case "created_at": + this.createdAt = BoxDateParser.parse(value.asString()); + break; + case "modified_at": + this.modifiedAt = BoxDateParser.parse(value.asString()); + break; + default: + break; + } + } catch (ParseException e) { + assert false : "A ParseException indicates a bug in the SDK."; + } + } + } +} diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index 71aa60b47..7f6c21af7 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -12,6 +12,10 @@ import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; +/** + * Represents an individual file on Box. This class can be used to download a file's contents, upload new versions, and + * perform other common file operations (move, copy, delete, etc.). + */ 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"); @@ -22,6 +26,11 @@ public class BoxFile extends BoxItem { private final URL fileURL; private final URL contentURL; + /** + * Constructs a BoxFile for a file with a given ID. + * @param api the API connection to be used by the file. + * @param id the ID of the file. + */ public BoxFile(BoxAPIConnection api, String id) { super(api, id); @@ -29,27 +38,64 @@ public BoxFile(BoxAPIConnection api, String id) { this.contentURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); } + /** + * Creates a new shared link for this file. + * + *

This method is a convenience method for manually creating a new shared link and applying it to this file with + * {@link Info#setSharedLink}. You may want to create the shared link manually so that it can be updated along with + * other changes to the file's info in a single network request, giving a boost to performance.

+ * + * @param access the access level of the shared link. + * @param unshareDate the date and time at which the link will expire. Can be null to create a non-expiring link. + * @param permissions the permissions of the shared link. Can be null to use the default permissions. + * @return the created shared link. + */ + public BoxSharedLink createSharedLink(BoxSharedLink.Access access, Date unshareDate, + BoxSharedLink.Permissions permissions) { + + BoxSharedLink sharedLink = new BoxSharedLink(); + sharedLink.setAccess(access); + + if (unshareDate != null) { + sharedLink.setUnsharedDate(unshareDate); + } + + sharedLink.setPermissions(permissions); + + Info info = new Info(); + info.setSharedLink(sharedLink); + + this.updateInfo(info); + return info.getSharedLink(); + } + + /** + * Downloads the contents of this file to a given OutputStream. + * @param output the stream to where the file will be written. + */ public void download(OutputStream output) { this.download(output, null); } + /** + * Downloads the contents of this file to a given OutputStream while reporting the progress to a ProgressListener. + * @param output the stream to where the file will be written. + * @param listener a listener for monitoring the download's progress. + */ public void download(OutputStream output, ProgressListener listener) { BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.contentURL, "GET"); BoxAPIResponse response = request.send(); InputStream input = response.getBody(); + if (listener != null) { + input = new ProgressInputStream(input, listener, response.getContentLength()); + } - 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); @@ -58,10 +104,12 @@ public void download(OutputStream output, ProgressListener listener) { response.disconnect(); } + @Override public BoxFile.Info copy(BoxFolder destination) { return this.copy(destination, null); } + @Override public BoxFile.Info copy(BoxFolder destination, String newName) { URL url = COPY_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); @@ -82,18 +130,23 @@ public BoxFile.Info copy(BoxFolder destination, String newName) { return copiedFile.new Info(responseJSON); } + /** + * Deletes this file by moving it to the trash. + */ public void delete() { BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.fileURL, "DELETE"); BoxAPIResponse response = request.send(); response.disconnect(); } + @Override public BoxFile.Info getInfo() { BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.fileURL, "GET"); BoxJSONResponse response = (BoxJSONResponse) request.send(); return new Info(response.getJSON()); } + @Override 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()); @@ -103,6 +156,19 @@ public BoxFile.Info getInfo(String... fields) { return new Info(response.getJSON()); } + /** + * Updates the information about this file with any info fields that have been modified locally. + * + *

The only fields that will be updated are the ones that have been modified locally. For example, the following + * code won't update any information (or even send a network request) since none of the info's fields were + * changed:

+ * + *
BoxFile file = new File(api, id);
+     *BoxFile.Info info = file.getInfo();
+     *file.updateInfo(info);
+ * + * @param info the updated info. + */ 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"); @@ -112,6 +178,11 @@ public void updateInfo(BoxFile.Info info) { info.update(jsonObject); } + /** + * Gets any previous versions of this file. Note that only users with premium accounts will be able to retrieve + * previous versions of their files. + * @return a list of previous file versions. + */ public Collection getVersions() { URL url = VERSIONS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); @@ -127,34 +198,81 @@ public Collection getVersions() { return versions; } + /** + * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts + * will be able to view and recover previous versions of the file. + * @param fileContent a stream containing the new file contents. + */ public void uploadVersion(InputStream fileContent) { this.uploadVersion(fileContent, null); } + /** + * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts + * will be able to view and recover previous versions of the file. + * @param fileContent a stream containing the new file contents. + * @param modified the date that the new version was modified. + */ public void uploadVersion(InputStream fileContent, Date modified) { + this.uploadVersion(fileContent, modified, 0, null); + } + + /** + * Uploads a new version of this file, replacing the current version, while reporting the progress to a + * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions + * of the file. + * @param fileContent a stream containing the new file contents. + * @param modified the date that the new version was modified. + * @param fileSize the size of the file used for determining the progress of the upload. + * @param listener a listener for monitoring the upload's progress. + */ + public void uploadVersion(InputStream fileContent, Date modified, long fileSize, ProgressListener listener) { URL uploadURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID()); BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL); - request.setFile(fileContent, ""); + if (fileSize > 0) { + request.setFile(fileContent, "", fileSize); + } else { + request.setFile(fileContent, ""); + } if (modified != null) { request.putField("content_modified_at", modified); } - BoxAPIResponse response = request.send(); + BoxAPIResponse response; + if (listener == null) { + response = request.send(); + } else { + response = request.send(listener); + } response.disconnect(); } - public class Info extends BoxItem.Info { + /** + * Contains additional information about a BoxFile. + */ + public class Info extends BoxItem.Info { private String sha1; + /** + * Constructs an empty Info object. + */ public Info() { super(); } + /** + * Constructs an Info object by parsing information from a JSON string. + * @param json the JSON string to parse. + */ public Info(String json) { super(json); } + /** + * Constructs an Info object using an already parsed JSON object. + * @param jsonObject the parsed JSON object. + */ protected Info(JsonObject jsonObject) { super(jsonObject); } @@ -164,6 +282,10 @@ public BoxFile getResource() { return BoxFile.this; } + /** + * Gets the SHA1 hash of the file. + * @return the SHA1 hash of the file. + */ public String getSha1() { return this.sha1; } diff --git a/src/main/java/com/box/sdk/BoxFolder.java b/src/main/java/com/box/sdk/BoxFolder.java index b742c59e9..84580fa50 100644 --- a/src/main/java/com/box/sdk/BoxFolder.java +++ b/src/main/java/com/box/sdk/BoxFolder.java @@ -2,12 +2,18 @@ import java.io.InputStream; import java.net.URL; -import java.util.Date; +import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; +/** + * Represents a folder on Box. This class can be used to iterate through a folder's contents, collaborate a folder with + * another user or group, and perform other common folder operations (move, copy, delete, etc.). + */ 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"); @@ -15,25 +21,121 @@ public final class BoxFolder extends BoxItem implements Iterable { 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"); + private static final URLTemplate ADD_COLLABORATION_URL = new URLTemplate("collaborations"); + private static final URLTemplate GET_COLLABORATIONS_URL = new URLTemplate("folders/%s/collaborations"); private final URL folderURL; + /** + * Constructs a BoxFolder for a folder with a given ID. + * @param api the API connection to be used by the folder. + * @param id the ID of the folder. + */ public BoxFolder(BoxAPIConnection api, String id) { super(api, id); this.folderURL = FOLDER_INFO_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); } + /** + * Gets the current user's root folder. + * @param api the API connection to be used by the folder. + * @return the user's root folder. + */ public static BoxFolder getRootFolder(BoxAPIConnection api) { return new BoxFolder(api, "0"); } + /** + * Adds a collaborator to this folder. + * @param collaborator the collaborator to add. + * @param role the role of the collaborator. + * @return info about the new collaboration. + */ + public BoxCollaboration.Info collaborate(BoxCollaborator collaborator, BoxCollaboration.Role role) { + JsonObject accessibleByField = new JsonObject(); + accessibleByField.add("id", collaborator.getID()); + + if (collaborator instanceof BoxUser) { + accessibleByField.add("type", "user"); + } else { + throw new IllegalArgumentException("The given collaborator is of an unknown type."); + } + + return this.collaborate(accessibleByField, role); + } + + /** + * Adds a collaborator to this folder. An email will be sent to the collaborator if they don't already have a Box + * account. + * @param email the email address of the collaborator to add. + * @param role the role of the collaborator. + * @return info about the new collaboration. + */ + public BoxCollaboration.Info collaborate(String email, BoxCollaboration.Role role) { + JsonObject accessibleByField = new JsonObject(); + accessibleByField.add("login", email); + accessibleByField.add("type", "user"); + + return this.collaborate(accessibleByField, role); + } + + private BoxCollaboration.Info collaborate(JsonObject accessibleByField, BoxCollaboration.Role role) { + BoxAPIConnection api = this.getAPI(); + URL url = ADD_COLLABORATION_URL.build(api.getBaseURL()); + + JsonObject itemField = new JsonObject(); + itemField.add("id", this.getID()); + itemField.add("type", "folder"); + + JsonObject requestJSON = new JsonObject(); + requestJSON.add("item", itemField); + requestJSON.add("accessible_by", accessibleByField); + requestJSON.add("role", role.toJSONString()); + + BoxJSONRequest request = new BoxJSONRequest(api, url, "POST"); + request.setBody(requestJSON.toString()); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); + + BoxCollaboration newCollaboration = new BoxCollaboration(api, responseJSON.get("id").asString()); + BoxCollaboration.Info info = newCollaboration.new Info(responseJSON); + return info; + } + + /** + * Gets information about all of the collaborations for this folder. + * @return a collection of information about the collaborations for this folder. + */ + public Collection getCollaborations() { + BoxAPIConnection api = this.getAPI(); + URL url = GET_COLLABORATIONS_URL.build(api.getBaseURL(), this.getID()); + + BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); + BoxJSONResponse response = (BoxJSONResponse) request.send(); + JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); + + int entriesCount = responseJSON.get("total_count").asInt(); + Collection collaborations = new ArrayList(entriesCount); + JsonArray entries = responseJSON.get("entries").asArray(); + for (JsonValue entry : entries) { + JsonObject entryObject = entry.asObject(); + BoxCollaboration collaboration = new BoxCollaboration(api, entryObject.get("id").asString()); + BoxCollaboration.Info info = collaboration.new Info(entryObject); + collaborations.add(info); + } + + return collaborations; + } + + @Override public BoxFolder.Info getInfo() { BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.folderURL, "GET"); BoxJSONResponse response = (BoxJSONResponse) request.send(); return new Info(response.getJSON()); } + @Override public BoxFolder.Info getInfo(String... fields) { String queryString = new QueryStringBuilder().addFieldsParam(fields).toString(); URL url = FOLDER_INFO_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID()); @@ -43,6 +145,10 @@ public BoxFolder.Info getInfo(String... fields) { return new Info(response.getJSON()); } + /** + * Updates the information about this folder with any info fields that have been modified locally. + * @param info the updated info. + */ public void updateInfo(BoxFolder.Info info) { BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), this.folderURL, "PUT"); request.setBody(info.getPendingChanges()); @@ -51,10 +157,12 @@ public void updateInfo(BoxFolder.Info info) { info.update(jsonObject); } + @Override public BoxFolder.Info copy(BoxFolder destination) { return this.copy(destination, null); } + @Override public BoxFolder.Info copy(BoxFolder destination, String newName) { URL url = COPY_FOLDER_URL.build(this.getAPI().getBaseURL(), this.getID()); BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST"); @@ -75,6 +183,11 @@ public BoxFolder.Info copy(BoxFolder destination, String newName) { return copiedFolder.new Info(responseJSON); } + /** + * Creates a new child folder inside this folder. + * @param name the new folder's name. + * @return the created folder. + */ public BoxFolder createFolder(String name) { JsonObject parent = new JsonObject(); parent.add("id", this.getID()); @@ -92,6 +205,10 @@ public BoxFolder createFolder(String name) { return new BoxFolder(this.getAPI(), createdFolder.get("id").asString()); } + /** + * Deletes this folder, optionally recursively deleting all of its contents. + * @param recursive true to recursively delete this folder's contents; otherwise false. + */ public void delete(boolean recursive) { URL url = DELETE_FOLDER_URL.build(this.getAPI().getBaseURL(), this.getID(), recursive); BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE"); @@ -99,15 +216,15 @@ public void delete(boolean recursive) { response.disconnect(); } + /** + * Moves this folder to another folder. + * @param destination the destination folder. + */ 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); + parent.add("id", destination.getID()); JsonObject updateInfo = new JsonObject(); updateInfo.add("parent", parent); @@ -117,6 +234,10 @@ public void move(String destinationID) { response.disconnect(); } + /** + * Renames this folder. + * @param newName the new name of the folder. + */ public void rename(String newName) { BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), this.folderURL, "PUT"); @@ -128,25 +249,66 @@ public void rename(String newName) { response.disconnect(); } + /** + * Uploads a new file to this folder. + * @param fileContent a stream containing the contents of the file to upload. + * @param name the name to give the uploaded file. + * @return the uploaded file. + */ public BoxFile uploadFile(InputStream fileContent, String name) { - return this.uploadFile(fileContent, name, null, null); + FileUploadParams uploadInfo = new FileUploadParams() + .setContent(fileContent) + .setName(name); + return this.uploadFile(uploadInfo); + } + + /** + * Uploads a new file to this folder while reporting the progress to a ProgressListener. + * @param fileContent a stream containing the contents of the file to upload. + * @param name the name to give the uploaded file. + * @param fileSize the size of the file used for determining the progress of the upload. + * @param listener a listener for monitoring the upload's progress. + * @return the uploaded file. + */ + public BoxFile uploadFile(InputStream fileContent, String name, long fileSize, ProgressListener listener) { + FileUploadParams uploadInfo = new FileUploadParams() + .setContent(fileContent) + .setName(name) + .setSize(fileSize) + .setProgressListener(listener); + return this.uploadFile(uploadInfo); } - public BoxFile uploadFile(InputStream fileContent, String name, Date created, Date modified) { + /** + * Uploads a new file to this folder with custom upload parameters. + * @param uploadParams the custom upload parameters. + * @return the uploaded file. + */ + public BoxFile uploadFile(FileUploadParams uploadParams) { 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 (uploadParams.getSize() > 0) { + request.setFile(uploadParams.getContent(), uploadParams.getName(), uploadParams.getSize()); + } else { + request.setFile(uploadParams.getContent(), uploadParams.getName()); } - if (modified != null) { - request.putField("content_modified_at", modified); + if (uploadParams.getCreated() != null) { + request.putField("content_created_at", uploadParams.getCreated()); } - BoxJSONResponse response = (BoxJSONResponse) request.send(); + if (uploadParams.getModified() != null) { + request.putField("content_modified_at", uploadParams.getModified()); + } + + BoxJSONResponse response; + if (uploadParams.getProgressListener() == null) { + response = (BoxJSONResponse) request.send(); + } else { + response = (BoxJSONResponse) request.send(uploadParams.getProgressListener()); + } JsonObject collection = JsonObject.readFrom(response.getJSON()); JsonArray entries = collection.get("entries").asArray(); String uploadedFileID = entries.get(0).asObject().get("id").asString(); @@ -154,19 +316,37 @@ public BoxFile uploadFile(InputStream fileContent, String name, Date created, Da return new BoxFile(getAPI(), uploadedFileID); } + /** + * Returns an iterator over the items in this folder. + * @return an iterator over the items in this folder. + */ public Iterator iterator() { return new BoxItemIterator(BoxFolder.this.getAPI(), BoxFolder.this.getID()); } - public class Info extends BoxItem.Info { + /** + * Contains additional information about a BoxFolder. + */ + public class Info extends BoxItem.Info { + /** + * Constructs an empty Info object. + */ public Info() { super(); } + /** + * Constructs an Info object by parsing information from a JSON string. + * @param json the JSON string to parse. + */ public Info(String json) { super(json); } + /** + * Constructs an Info object using an already parsed JSON object. + * @param jsonObject the parsed JSON object. + */ protected Info(JsonObject jsonObject) { super(jsonObject); } diff --git a/src/main/java/com/box/sdk/BoxItem.java b/src/main/java/com/box/sdk/BoxItem.java index f613b2687..dfd3d389e 100644 --- a/src/main/java/com/box/sdk/BoxItem.java +++ b/src/main/java/com/box/sdk/BoxItem.java @@ -9,12 +9,52 @@ import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; +/** + * The abstract base class for items in a user's file tree (files, folders, etc.). + */ public abstract class BoxItem extends BoxResource { + /** + * Constructs a BoxItem for an item with a given ID. + * @param api the API connection to be used by the item. + * @param id the ID of the item. + */ public BoxItem(BoxAPIConnection api, String id) { super(api, id); } - public abstract class Info extends BoxResource.Info { + /** + * Copies this item to another folder. + * @param destination the destination folder. + * @return info about the copied item. + */ + public abstract BoxItem.Info copy(BoxFolder destination); + + /** + * Copies this item to another folder and gives it a new name. If the destination is the same folder as the item's + * current parent, then newName must be a new, unique name. + * @param destination the destination folder. + * @param newName a new name for the copied item. + * @return info about the copied item. + */ + public abstract BoxItem.Info copy(BoxFolder destination, String newName); + + /** + * Gets additional information about this item. + * @return info about this item. + */ + public abstract BoxItem.Info getInfo(); + + /** + * Gets additional information about this item that's limited to a list of specified fields. + * @param fields the fields to retrieve. + * @return info about this item containing only the specified fields. + */ + public abstract BoxItem.Info getInfo(String... fields); + + /** + * Contains additional information about a BoxItem. + */ + public abstract class Info extends BoxResource.Info { private String sequenceID; private String etag; private String name; @@ -30,95 +70,204 @@ public abstract class Info extends BoxResource.Info { private Date contentCreatedAt; private Date contentModifiedAt; private BoxUser.Info ownedBy; + private BoxSharedLink sharedLink; private List tags; private BoxFolder.Info parent; + /** + * Constructs an empty Info object. + */ public Info() { super(); } + /** + * Constructs an Info object by parsing information from a JSON string. + * @param json the JSON string to parse. + */ public Info(String json) { super(json); } + /** + * Constructs an Info object using an already parsed JSON object. + * @param jsonObject the parsed JSON object. + */ protected Info(JsonObject jsonObject) { super(jsonObject); } + /** + * Gets a unique string identifying the version of the item. + * @return a unique string identifying the version of the item. + */ public String getEtag() { return this.etag; } + /** + * Gets the name of the item. + * @return the name of the item. + */ public String getName() { return this.name; } + /** + * Sets the name of the item. + * @param name the new name of the item. + */ public void setName(String name) { this.name = name; this.addPendingChange("name", name); } + /** + * Gets the time the item was created. + * @return the time the item was created. + */ public Date getCreatedAt() { return this.createdAt; } + /** + * Gets the time the item was last modified. + * @return the time the item was last modified. + */ public Date getModifiedAt() { return this.modifiedAt; } + /** + * Gets the description of the item. + * @return the description of the item. + */ public String getDescription() { return this.description; } + /** + * Sets the description of the item. + * @param description the new description of the item. + */ public void setDescription(String description) { this.description = description; this.addPendingChange("description", description); } + /** + * Gets the size of the item in bytes. + * @return the size of the item in bytes. + */ public long getSize() { return this.size; } + /** + * Gets the path of folders to the item, starting at the root. + * @return the path of folders to the item. + */ public List getPathCollection() { return this.pathCollection; } + /** + * Gets info about the user who created the item. + * @return info about the user who created the item. + */ public BoxUser.Info getCreatedBy() { return this.createdBy; } + /** + * Gets info about the user who last modified the item. + * @return info about the user who last modified the item. + */ public BoxUser.Info getModifiedBy() { return this.modifiedBy; } + /** + * Gets the time that the item was trashed. + * @return the time that the item was trashed. + */ public Date getTrashedAt() { return this.trashedAt; } + /** + * Gets the time that the item was purged from the trash. + * @return the time that the item was purged from the trash. + */ public Date getPurgedAt() { return this.purgedAt; } + /** + * Gets the time that the item was created according to the uploader. + * @return the time that the item was created according to the uploader. + */ public Date getContentCreatedAt() { return this.createdAt; } + /** + * Gets the time that the item was last modified according to the uploader. + * @return the time that the item was last modified according to the uploader. + */ public Date getContentModifiedAt() { return this.contentModifiedAt; } + /** + * Gets info about the user who owns the item. + * @return info about the user who owns the item. + */ public BoxUser.Info getOwnedBy() { return this.ownedBy; } + /** + * Gets the shared link for the item. + * @return the shared link for the item. + */ + public BoxSharedLink getSharedLink() { + return this.sharedLink; + } + + /** + * Sets a shared link for the item. + * @param sharedLink the shared link for the item. + */ + public void setSharedLink(BoxSharedLink sharedLink) { + this.sharedLink = sharedLink; + this.addPendingChange("shared_link", sharedLink); + } + + /** + * Gets a unique ID for use with the {@link EventStream}. + * @return a unique ID for use with the EventStream. + */ public String getSequenceID() { return this.sequenceID; } + /** + * Gets a list of all the tags applied to the item. + * + *

Note that this field isn't populated by default and must be specified as a field parameter when getting + * Info about the item.

+ * + * @return a list of all the tags applied to the item. + */ public List getTags() { return this.tags; } + /** + * Gets info about the parent folder of the item. + * @return info abou thte parent folder of the item. + */ public BoxFolder.Info getParent() { return this.parent; } @@ -175,6 +324,9 @@ protected void parseJSONMember(JsonObject.Member member) { case "owned_by": this.ownedBy = this.parseUserInfo(value.asObject()); break; + case "shared_link": + this.sharedLink = new BoxSharedLink(value.asObject()); + break; case "tags": this.tags = this.parseTags(value.asArray()); break; diff --git a/src/main/java/com/box/sdk/BoxJSONObject.java b/src/main/java/com/box/sdk/BoxJSONObject.java index b1e3a9669..dfb207c1f 100644 --- a/src/main/java/com/box/sdk/BoxJSONObject.java +++ b/src/main/java/com/box/sdk/BoxJSONObject.java @@ -150,7 +150,7 @@ void update(JsonObject jsonObject) { * 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() { + private JsonObject getPendingJSONObject() { if (this.pendingChanges == null && !this.lazyPendingChanges.isEmpty()) { this.pendingChanges = new JsonObject(); } diff --git a/src/main/java/com/box/sdk/BoxMultipartRequest.java b/src/main/java/com/box/sdk/BoxMultipartRequest.java index fae186c6e..4e09839a0 100644 --- a/src/main/java/com/box/sdk/BoxMultipartRequest.java +++ b/src/main/java/com/box/sdk/BoxMultipartRequest.java @@ -15,12 +15,14 @@ public class BoxMultipartRequest extends BoxAPIRequest { private static final Logger LOGGER = Logger.getLogger(BoxFolder.class.getName()); private static final String BOUNDARY = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + private static final int BUFFER_SIZE = 8192; private final StringBuilder loggedRequest = new StringBuilder(); private OutputStream outputStream; private InputStream inputStream; private String filename; + private long fileSize; private Map fields; private boolean firstBoundary; @@ -46,6 +48,11 @@ public void setFile(InputStream inputStream, String filename) { this.filename = filename; } + public void setFile(InputStream inputStream, String filename, long fileSize) { + this.setFile(inputStream, filename); + this.fileSize = fileSize; + } + @Override public void setBody(InputStream stream) { throw new UnsupportedOperationException(); @@ -57,7 +64,7 @@ public void setBody(String body) { } @Override - public void writeBody(HttpURLConnection connection) { + public void writeBody(HttpURLConnection connection, ProgressListener listener) { try { connection.setChunkedStreamingMode(0); connection.setDoOutput(true); @@ -65,10 +72,16 @@ public void writeBody(HttpURLConnection connection) { this.writePartHeader(new String[][] {{"name", "filename"}, {"filename", this.filename}}, "application/octet-stream"); - int b = this.inputStream.read(); - while (b != -1) { - this.writeOutput(b); - b = this.inputStream.read(); + + OutputStream fileContentsOutputStream = this.outputStream; + if (listener != null) { + fileContentsOutputStream = new ProgressOutputStream(this.outputStream, listener, this.fileSize); + } + byte[] buffer = new byte[BUFFER_SIZE]; + int n = this.inputStream.read(buffer); + while (n != -1) { + fileContentsOutputStream.write(buffer, 0, n); + n = this.inputStream.read(buffer); } if (LOGGER.isLoggable(Level.INFO)) { diff --git a/src/main/java/com/box/sdk/BoxResource.java b/src/main/java/com/box/sdk/BoxResource.java index 759a5b4a4..ae48866a9 100644 --- a/src/main/java/com/box/sdk/BoxResource.java +++ b/src/main/java/com/box/sdk/BoxResource.java @@ -7,7 +7,6 @@ * *

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; @@ -51,7 +50,7 @@ public boolean equals(Object other) { return false; } - if (other instanceof BoxResource) { + if (this.getClass().equals(other.getClass())) { BoxResource otherResource = (BoxResource) other; return this.getID().equals(otherResource.getID()); } @@ -73,10 +72,8 @@ public int hashCode() { * *

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 extends BoxJSONObject { + public abstract class Info extends BoxJSONObject { /** * Constructs an empty Info object. */ @@ -112,6 +109,6 @@ public String getID() { * Gets the resource associated with this Info. * @return the associated resource. */ - public abstract T getResource(); + public abstract BoxResource getResource(); } } diff --git a/src/main/java/com/box/sdk/BoxSharedLink.java b/src/main/java/com/box/sdk/BoxSharedLink.java new file mode 100644 index 000000000..30e8161d6 --- /dev/null +++ b/src/main/java/com/box/sdk/BoxSharedLink.java @@ -0,0 +1,191 @@ +package com.box.sdk; + +import java.text.ParseException; +import java.util.Date; + +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; + +public class BoxSharedLink extends BoxJSONObject { + private String url; + private String downloadUrl; + private String vanityUrl; + private boolean isPasswordEnabled; + private Date unsharedAt; + private long downloadCount; + private long previewCount; + private Access access; + private Permissions permissions; + + public enum Access { + DEFAULT (null), + OPEN ("open"), + COMPANY ("company"), + COLLABORATORS ("collaborators"); + + private final String jsonValue; + + private Access(String jsonValue) { + this.jsonValue = jsonValue; + } + + @Override + public String toString() { + return this.jsonValue; + } + } + + public BoxSharedLink() { } + + public BoxSharedLink(String json) { + super(json); + } + + BoxSharedLink(JsonObject jsonObject) { + super(jsonObject); + } + + public String getURL() { + return this.url; + } + + public String getDownloadURL() { + return this.downloadUrl; + } + + public String getVanityURL() { + return this.vanityUrl; + } + + public boolean getIsPasswordEnabled() { + return this.isPasswordEnabled; + } + + public Date getUnsharedDate() { + return this.unsharedAt; + } + + public void setUnsharedDate(Date unsharedDate) { + this.unsharedAt = unsharedDate; + this.addPendingChange("unshared_at", unsharedDate.toString()); + } + + public long getDownloadCount() { + return this.downloadCount; + } + + public long getPreviewCount() { + return this.previewCount; + } + + public Access getAccess() { + return this.access; + } + + public void setAccess(Access access) { + this.access = access; + this.addPendingChange("access", access.toString()); + } + + public Permissions getPermissions() { + return this.permissions; + } + + public void setPermissions(Permissions permissions) { + this.permissions = permissions; + this.addPendingChange("permissions", permissions); + } + + @Override + void parseJSONMember(JsonObject.Member member) { + JsonValue value = member.getValue(); + try { + switch (member.getName()) { + case "url": + this.url = value.asString(); + break; + case "download_url": + this.downloadUrl = value.asString(); + break; + case "vanity_url": + this.vanityUrl = value.asString(); + break; + case "is_password_enabled": + this.isPasswordEnabled = value.asBoolean(); + break; + case "unshared_at": + this.unsharedAt = BoxDateParser.parse(value.asString()); + break; + case "download_count": + this.downloadCount = Double.valueOf(value.toString()).longValue(); + break; + case "preview_count": + this.previewCount = Double.valueOf(value.toString()).longValue(); + break; + case "access": + String accessString = value.asString().toUpperCase(); + this.access = Access.valueOf(accessString); + break; + case "permissions": + if (this.permissions == null) { + this.permissions = new Permissions(value.asObject()); + } else { + this.permissions.update(value.asObject()); + } + break; + default: + break; + } + } catch (ParseException e) { + assert false : "A ParseException indicates a bug in the SDK."; + } + } + + public static class Permissions extends BoxJSONObject { + private boolean canDownload; + private boolean canPreview; + + public Permissions() { } + + public Permissions(String json) { + super(json); + } + + Permissions(JsonObject jsonObject) { + super(jsonObject); + } + + public boolean getCanDownload() { + return this.canDownload; + } + + public void setCanDownload(boolean enabled) { + this.canDownload = enabled; + this.addPendingChange("can_download", enabled); + } + + public boolean getCanPreview() { + return this.canPreview; + } + + public void setCanPreview(boolean enabled) { + this.canPreview = enabled; + this.addPendingChange("can_preview", enabled); + } + + @Override + void parseJSONMember(JsonObject.Member member) { + JsonValue value = member.getValue(); + switch (member.getName()) { + case "can_download": + this.canDownload = value.asBoolean(); + break; + case "can_preview": + this.canPreview = value.asBoolean(); + break; + default: + break; + } + } + } +} diff --git a/src/main/java/com/box/sdk/BoxUser.java b/src/main/java/com/box/sdk/BoxUser.java index e337f88e7..1951017a3 100644 --- a/src/main/java/com/box/sdk/BoxUser.java +++ b/src/main/java/com/box/sdk/BoxUser.java @@ -7,7 +7,7 @@ import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; -public class BoxUser extends BoxResource { +public class BoxUser extends BoxCollaborator { private static final URLTemplate GET_USER_URL = new URLTemplate("users/%s"); private static final URLTemplate GET_ME_URL = new URLTemplate("users/me"); @@ -44,7 +44,7 @@ public enum Status { CANNOT_DELETE_EDIT_UPLOAD } - public class Info extends BoxResource.Info { + public class Info extends BoxCollaborator.Info { private String name; private String login; private Date createdAt; diff --git a/src/main/java/com/box/sdk/FileUploadParams.java b/src/main/java/com/box/sdk/FileUploadParams.java new file mode 100644 index 000000000..804dce576 --- /dev/null +++ b/src/main/java/com/box/sdk/FileUploadParams.java @@ -0,0 +1,67 @@ +package com.box.sdk; + +import java.io.InputStream; +import java.util.Date; + +public class FileUploadParams { + private InputStream content; + private String name; + private Date created; + private Date modified; + private long size; + private ProgressListener listener; + + public InputStream getContent() { + return this.content; + } + + public FileUploadParams setContent(InputStream content) { + this.content = content; + return this; + } + + public String getName() { + return this.name; + } + + public FileUploadParams setName(String name) { + this.name = name; + return this; + } + + public Date getCreated() { + return this.created; + } + + public FileUploadParams setCreated(Date created) { + this.created = created; + return this; + } + + public Date getModified() { + return this.modified; + } + + public FileUploadParams setModified(Date modified) { + this.modified = modified; + return this; + } + + public long getSize() { + return this.size; + } + + public FileUploadParams setSize(long size) { + this.size = size; + return this; + } + + public ProgressListener getProgressListener() { + return this.listener; + } + + public FileUploadParams setProgressListener(ProgressListener listener) { + this.listener = listener; + return this; + } +} diff --git a/src/main/java/com/box/sdk/ProgressInputStream.java b/src/main/java/com/box/sdk/ProgressInputStream.java new file mode 100644 index 000000000..3c963f825 --- /dev/null +++ b/src/main/java/com/box/sdk/ProgressInputStream.java @@ -0,0 +1,63 @@ +package com.box.sdk; + +import java.io.IOException; +import java.io.InputStream; + +/** + * An {@link InputStream} that can report the progress of reading from another InputStream to a + * {@link ProgressListener}. + */ +public class ProgressInputStream extends InputStream { + private final InputStream stream; + private final ProgressListener listener; + + private long total; + private long totalRead; + private int progress; + + /** + * Constructs a ProgressInputStream that wraps another InputStream. + * @param stream the stream whose progress will be monitored. + * @param listener the listener that will receive progress updates. + * @param total the total number of bytes that are expected to be read from the stream. + */ + public ProgressInputStream(InputStream stream, ProgressListener listener, long total) { + this.stream = stream; + this.listener = listener; + this.total = total; + } + + /** + * Gets the total number of bytes that are expected to be read from the stream. + * @return the total number of bytes. + */ + public long getTotal() { + return this.total; + } + + /** + * Sets the total number of bytes that are expected to be read from the stream. + * @param total the total number of bytes + */ + public void setTotal(long total) { + this.total = total; + } + + @Override + public int read() throws IOException { + int read = this.stream.read(); + this.totalRead += read; + this.listener.onProgressChanged(this.totalRead, this.total); + + return read; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = this.stream.read(b, off, len); + this.totalRead += read; + this.listener.onProgressChanged(this.totalRead, this.total); + + return read; + } +} diff --git a/src/main/java/com/box/sdk/ProgressOutputStream.java b/src/main/java/com/box/sdk/ProgressOutputStream.java new file mode 100644 index 000000000..9f7955bb6 --- /dev/null +++ b/src/main/java/com/box/sdk/ProgressOutputStream.java @@ -0,0 +1,56 @@ +package com.box.sdk; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An {@link OutputStream} that can report the progress of writing to another OutputStream to a + * {@link ProgressListener}. + */ +public class ProgressOutputStream extends OutputStream { + private final OutputStream stream; + private final ProgressListener listener; + + private long total; + private long totalWritten; + private int progress; + + public ProgressOutputStream(OutputStream stream, ProgressListener listener, long total) { + this.stream = stream; + this.listener = listener; + this.total = total; + } + + public long getTotal() { + return this.total; + } + + public void setTotal(long total) { + this.total = total; + } + + @Override + public void write(byte[] b) throws IOException { + this.stream.write(b); + this.totalWritten += b.length; + this.listener.onProgressChanged(this.totalWritten, this.total); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.stream.write(b, off, len); + if (len < b.length) { + this.totalWritten += len; + } else { + this.totalWritten += b.length; + } + this.listener.onProgressChanged(this.totalWritten, this.total); + } + + @Override + public void write(int b) throws IOException { + this.stream.write(b); + this.totalWritten++; + this.listener.onProgressChanged(this.totalWritten, this.total); + } +} diff --git a/src/test/config/config.properties.template b/src/test/config/config.properties.template index 7466e5e0a..fa5ee651f 100644 --- a/src/test/config/config.properties.template +++ b/src/test/config/config.properties.template @@ -6,3 +6,4 @@ accessToken = refreshToken = clientID = clientSecret = +collaborator = diff --git a/src/test/java/com/box/sdk/BoxCollaborationTest.java b/src/test/java/com/box/sdk/BoxCollaborationTest.java new file mode 100644 index 000000000..73e175060 --- /dev/null +++ b/src/test/java/com/box/sdk/BoxCollaborationTest.java @@ -0,0 +1,53 @@ +package com.box.sdk; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +public class BoxCollaborationTest { + @Test + @Category(IntegrationTest.class) + public void updateInfoSucceeds() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + String folderName = "[addCollaborationToFolderSucceeds] Test Folder"; + String collaboratorLogin = TestConfig.getCollaborator(); + BoxCollaboration.Role originalRole = BoxCollaboration.Role.VIEWER; + BoxCollaboration.Role newRole = BoxCollaboration.Role.EDITOR; + + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder folder = rootFolder.createFolder(folderName); + + BoxCollaboration.Info collabInfo = folder.collaborate(collaboratorLogin, originalRole); + + assertThat(collabInfo.getRole(), is(equalTo(originalRole))); + + BoxCollaboration collab = collabInfo.getResource(); + collabInfo.setRole(newRole); + collab.updateInfo(collabInfo); + + assertThat(collabInfo.getRole(), is(equalTo(newRole))); + + folder.delete(false); + } + + @Test + @Category(IntegrationTest.class) + public void deleteSucceeds() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + String folderName = "[deleteSucceeds] Test Folder"; + String collaboratorLogin = TestConfig.getCollaborator(); + BoxCollaboration.Role collaboratorRole = BoxCollaboration.Role.EDITOR; + + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder folder = rootFolder.createFolder(folderName); + + BoxCollaboration.Info collabInfo = folder.collaborate(collaboratorLogin, collaboratorRole); + BoxCollaboration collab = collabInfo.getResource(); + collab.delete(); + + folder.delete(false); + } +} diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index e0b9feee5..4121a5afe 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -2,19 +2,29 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Collection; 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.isEmptyOrNullString; +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.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -24,25 +34,28 @@ public class BoxFileTest { @Test @Category(IntegrationTest.class) - public void uploadAndDownloadFileSucceeds() throws UnsupportedEncodingException { + public void uploadAndDownloadFileSucceeds() throws IOException { 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; + String fileName = "Tamme-Lauri_tamm_suvepäeval.jpg"; + URL fileURL = this.getClass().getResource("/sample-files/" + fileName); + String filePath = URLDecoder.decode(fileURL.getFile(), "utf-8"); + long fileSize = new File(filePath).length(); + byte[] fileContent = Files.readAllBytes(Paths.get(filePath)); - InputStream uploadStream = new ByteArrayInputStream(fileBytes); - BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); + InputStream uploadStream = new FileInputStream(filePath); + ProgressListener mockUploadListener = mock(ProgressListener.class); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName, fileSize, mockUploadListener); ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); - ProgressListener mockProgressListener = mock(ProgressListener.class); - uploadedFile.download(downloadStream, mockProgressListener); - String downloadedContent = downloadStream.toString(StandardCharsets.UTF_8.name()); + ProgressListener mockDownloadListener = mock(ProgressListener.class); + uploadedFile.download(downloadStream, mockDownloadListener); + byte[] downloadedFileContent = downloadStream.toByteArray(); + assertThat(downloadedFileContent, is(equalTo(fileContent))); assertThat(rootFolder, hasItem(uploadedFile)); - assertThat(downloadedContent, equalTo(fileContent)); - verify(mockProgressListener).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize)))); + verify(mockUploadListener, atLeastOnce()).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize)))); + verify(mockDownloadListener, atLeastOnce()).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize)))); uploadedFile.delete(); } @@ -63,21 +76,24 @@ public void uploadAndDownloadMultipleVersionsSucceeds() throws UnsupportedEncodi InputStream uploadStream = new ByteArrayInputStream(version1Bytes); BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); + uploadStream = new ByteArrayInputStream(version2Bytes); - uploadedFile.uploadVersion(uploadStream); + ProgressListener mockUploadListener = mock(ProgressListener.class); + uploadedFile.uploadVersion(uploadStream, null, version2Size, mockUploadListener); Collection versions = uploadedFile.getVersions(); BoxFileVersion previousVersion = versions.iterator().next(); ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); - ProgressListener mockProgressListener = mock(ProgressListener.class); - previousVersion.download(downloadStream, mockProgressListener); + ProgressListener mockDownloadListener = mock(ProgressListener.class); + previousVersion.download(downloadStream, mockDownloadListener); String downloadedContent = downloadStream.toString(StandardCharsets.UTF_8.name()); assertThat(versions, hasSize(1)); assertThat(previousVersion.getSha1(), is(equalTo(version1Sha))); assertThat(downloadedContent, equalTo(version1Content)); - verify(mockProgressListener).onProgressChanged(anyLong(), longThat(is(equalTo(version1Size)))); + verify(mockDownloadListener).onProgressChanged(anyLong(), longThat(is(equalTo(version1Size)))); + verify(mockUploadListener).onProgressChanged(anyLong(), longThat(is(equalTo(version1Size)))); uploadedFile.delete(); } @@ -197,4 +213,24 @@ public void copyFileSucceeds() throws UnsupportedEncodingException { uploadedFile.delete(); copiedFile.delete(); } + + @Test + @Category(IntegrationTest.class) + public void createSharedLinkSucceeds() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + String fileName = "[createSharedLinkSucceeds] Test File.txt"; + byte[] fileBytes = "Non-empty string".getBytes(StandardCharsets.UTF_8); + + InputStream uploadStream = new ByteArrayInputStream(fileBytes); + BoxFile uploadedFile = rootFolder.uploadFile(uploadStream, fileName); + BoxSharedLink.Permissions permissions = new BoxSharedLink.Permissions(); + permissions.setCanDownload(true); + permissions.setCanPreview(true); + BoxSharedLink sharedLink = uploadedFile.createSharedLink(BoxSharedLink.Access.OPEN, null, permissions); + + assertThat(sharedLink.getURL(), not(isEmptyOrNullString())); + + uploadedFile.delete(); + } } diff --git a/src/test/java/com/box/sdk/BoxFolderTest.java b/src/test/java/com/box/sdk/BoxFolderTest.java index 143a4db23..999c37eb3 100644 --- a/src/test/java/com/box/sdk/BoxFolderTest.java +++ b/src/test/java/com/box/sdk/BoxFolderTest.java @@ -3,15 +3,19 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.List; 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; import static org.junit.Assert.assertThat; +import org.hamcrest.Matchers; + import org.junit.Test; import org.junit.experimental.categories.Category; @@ -95,7 +99,7 @@ public void uploadFileSucceeds() { final String fileContent = "Test file"; InputStream stream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8)); - BoxFile uploadedFile = rootFolder.uploadFile(stream, "Test File.txt", null, null); + BoxFile uploadedFile = rootFolder.uploadFile(stream, "Test File.txt"); assertThat(rootFolder, hasItem(uploadedFile)); @@ -183,4 +187,45 @@ public void renameFolderSucceeds() { childFolder.delete(false); assertThat(rootFolder, not(hasItem(childFolder))); } + + @Test + @Category(IntegrationTest.class) + public void addCollaboratorSucceeds() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + String folderName = "[addCollaborationToFolderSucceeds] Test Folder"; + String collaboratorLogin = TestConfig.getCollaborator(); + BoxCollaboration.Role collaboratorRole = BoxCollaboration.Role.CO_OWNER; + + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder folder = rootFolder.createFolder(folderName); + + BoxCollaboration.Info collabInfo = folder.collaborate(collaboratorLogin, collaboratorRole); + BoxUser.Info accessibleBy = (BoxUser.Info) collabInfo.getAccessibleBy(); + + assertThat(accessibleBy.getLogin(), is(equalTo(collaboratorLogin))); + assertThat(collabInfo.getRole(), is(equalTo(collaboratorRole))); + + folder.delete(false); + } + + @Test + @Category(IntegrationTest.class) + public void getCollaborationsHasCorrectCollaborations() { + BoxAPIConnection api = new BoxAPIConnection(TestConfig.getAccessToken()); + String folderName = "[getCollaborationsSucceeds] Test Folder"; + String collaboratorLogin = TestConfig.getCollaborator(); + BoxCollaboration.Role collaboratorRole = BoxCollaboration.Role.CO_OWNER; + + BoxFolder rootFolder = BoxFolder.getRootFolder(api); + BoxFolder folder = rootFolder.createFolder(folderName); + BoxCollaboration.Info collabInfo = folder.collaborate(collaboratorLogin, collaboratorRole); + String collabID = collabInfo.getID(); + + Collection collaborations = folder.getCollaborations(); + + assertThat(collaborations, hasSize(1)); + assertThat(collaborations, hasItem(Matchers.hasProperty("ID", equalTo(collabID)))); + + folder.delete(false); + } } diff --git a/src/test/java/com/box/sdk/TestConfig.java b/src/test/java/com/box/sdk/TestConfig.java index 568306522..4b802a7a7 100644 --- a/src/test/java/com/box/sdk/TestConfig.java +++ b/src/test/java/com/box/sdk/TestConfig.java @@ -11,6 +11,7 @@ final class TestConfig { private static String refreshToken = null; private static String clientID = null; private static String clientSecret = null; + private static String collaborator = null; private TestConfig() { } @@ -54,6 +55,14 @@ public static String getClientSecret() { return clientSecret; } + public static String getCollaborator() { + if (collaborator == null || collaborator.equals("")) { + collaborator = getProperty("collaborator"); + } + + return collaborator; + } + private static String getProperty(String name) { Properties configProperties = loadProperties(); String value = configProperties.getProperty(name); diff --git "a/src/test/resources/sample-files/Tamme-Lauri_tamm_suvep\303\244eval.jpg" "b/src/test/resources/sample-files/Tamme-Lauri_tamm_suvep\303\244eval.jpg" new file mode 100644 index 000000000..b2fc3dfff Binary files /dev/null and "b/src/test/resources/sample-files/Tamme-Lauri_tamm_suvep\303\244eval.jpg" differ