Skip to content

Commit

Permalink
Batch api (#461)
Browse files Browse the repository at this point in the history
* batch api initial support for list of BoxAPIRequests
* batch example and unit tests
  • Loading branch information
iamharish committed Aug 28, 2017
1 parent 44ae732 commit 744ad33
Show file tree
Hide file tree
Showing 43 changed files with 871 additions and 162 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -120,6 +120,12 @@ Then just invoke `gradle runAccessAsAppUser` to run the AccessAsAppUser example!

Note: The JCE bundled with oracle JRE supports keys upto 128 bit length only. To use larger cryptographic keys, install [JCE Unlimited Strength Jurisdiction Policy Files](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html).

#### BatchRequestExample

There migt be cases where you want a bunch of requests to be executed as a single request. This example will show you how to do that

Just invoke `gradle runBatchExample` to run the BatchRequestExample example!

Building
--------

Expand Down
24 changes: 15 additions & 9 deletions build.gradle
Expand Up @@ -15,6 +15,15 @@ repositories {
mavenCentral()
}

sourceSets {
example {
java {
compileClasspath += main.output
runtimeClasspath += main.runtimeClasspath
}
}
}

dependencies {
compile 'com.eclipsesource.minimal-json:minimal-json:0.9.1'
compile 'org.bitbucket.b_c:jose4j:0.4.4'
Expand All @@ -26,6 +35,7 @@ dependencies {
testCompile 'org.mockito:mockito-core:1.9.5'
testCompile 'org.slf4j:slf4j-api:1.7.7'
testCompile 'org.slf4j:slf4j-nop:1.7.7'
exampleCompile 'com.eclipsesource.minimal-json:minimal-json:0.9.1'
}

compileJava {
Expand All @@ -52,15 +62,6 @@ javadoc {
checkstyleMain { classpath += configurations.compile }
checkstyleTest { classpath += configurations.compile }

sourceSets {
example {
java {
compileClasspath += main.output
runtimeClasspath += main.runtimeClasspath
}
}
}

task runExample(type: JavaExec, dependsOn: 'exampleClasses') {
classpath = sourceSets.example.runtimeClasspath
main = 'com.box.sdk.example.Main'
Expand All @@ -76,6 +77,11 @@ task runAccessAsAppUser(type: JavaExec, dependsOn: 'exampleClasses') {
main = 'com.box.sdk.example.AccessAsAppUser'
}

task runBatchExample(type: JavaExec, dependsOn: 'exampleClasses') {
classpath = sourceSets.example.runtimeClasspath
main = 'com.box.sdk.example.BatchRequestExample'
}

task javadocJar(type: Jar) {
classifier = 'javadoc'
from javadoc
Expand Down
4 changes: 4 additions & 0 deletions config/checkstyle/suppressions.xml
Expand Up @@ -11,4 +11,8 @@
files="example.*\.java"/>
<suppress checks="ImportOrder"
files="test.*\.java"/>
<suppress checks="DeclarationOrderCheck"
files="main/java/com/box/sdk/EventStream.java"/>
<suppress checks="DeclarationOrderCheck"
files="main/java/com/box/sdk/EventLog.java"/>
</suppressions>
87 changes: 87 additions & 0 deletions src/example/java/com/box/sdk/example/BatchRequestExample.java
@@ -0,0 +1,87 @@
package com.box.sdk.example;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.box.sdk.BatchAPIRequest;
import com.box.sdk.BoxAPIRequest;
import com.box.sdk.BoxAPIResponse;
import com.box.sdk.BoxConfig;
import com.box.sdk.BoxDeveloperEditionAPIConnection;
import com.box.sdk.BoxFolder;
import com.box.sdk.BoxJSONRequest;
import com.box.sdk.BoxJSONResponse;
import com.box.sdk.BoxUser;
import com.box.sdk.IAccessTokenCache;
import com.box.sdk.InMemoryLRUAccessTokenCache;
import com.box.sdk.http.HttpMethod;

/**
*
*/
public final class BatchRequestExample {

private static final int MAX_CACHE_ENTRIES = 100;
private static final String APP_USER_NAME = "BATCH-TEST-APP-USER-NAME";

private BatchRequestExample() { }

public static void main(String[] args) throws IOException {
// Turn off logging to prevent polluting the output.
Logger.getLogger("com.box.sdk").setLevel(Level.OFF);


//It is a best practice to use an access token cache to prevent unneeded requests to Box for access tokens.
//For production applications it is recommended to use a distributed cache like Memcached or Redis, and to
//implement IAccessTokenCache to store and retrieve access tokens appropriately for your environment.
IAccessTokenCache accessTokenCache = new InMemoryLRUAccessTokenCache(MAX_CACHE_ENTRIES);

Reader reader = new FileReader("src/example/config/config.json");
BoxConfig boxConfig = BoxConfig.readFrom(reader);

BoxDeveloperEditionAPIConnection api = BoxDeveloperEditionAPIConnection.getAppEnterpriseConnection(
boxConfig, accessTokenCache);

List<BoxAPIRequest> requests = new ArrayList<BoxAPIRequest>();

//Get current user request
URL getMeURL = BoxUser.GET_ME_URL.build(api.getBaseURL());
BoxAPIRequest getMeRequest = new BoxAPIRequest(getMeURL, HttpMethod.GET);
requests.add(getMeRequest);

//Create App User Request
URL createUserURL = BoxUser.USERS_URL_TEMPLATE.build(api.getBaseURL());
BoxJSONRequest createAppUserRequest = new BoxJSONRequest(createUserURL, HttpMethod.POST);
createAppUserRequest.setBody("{\"name\":\"" + APP_USER_NAME + "\",\"is_platform_access_only\":true}");
requests.add(createAppUserRequest);

//Get Root Folder Request
URL getRootFolderURL = BoxFolder.FOLDER_INFO_URL_TEMPLATE.build(api.getBaseURL(), 0);
BoxAPIRequest getRootFolderRequest = new BoxAPIRequest(getRootFolderURL, HttpMethod.GET);
requests.add(getRootFolderRequest);

BatchAPIRequest batchRequest = new BatchAPIRequest(api);
List<BoxAPIResponse> responses = batchRequest.execute(requests);

System.out.println("GET ME RESPONSE:");
System.out.println("Response Code: " + responses.get(0).getResponseCode());
System.out.println("Response: " + ((BoxJSONResponse) responses.get(1)).getJSON());

System.out.println("CREATE APP USER RESPONSE:");
System.out.println("Response Code: " + responses.get(1).getResponseCode());
System.out.println("Response: " + ((BoxJSONResponse) responses.get(1)).getJSON());

System.out.println("GET ROOT FOLDER RESPONSE:");
System.out.println("Response Code: " + responses.get(2).getResponseCode());
BoxJSONResponse rootFolderResponse = (BoxJSONResponse) responses.get(1);
BoxFolder.Info rootFolderInfo = new BoxFolder(api, rootFolderResponse.getJsonObject().get("id").asString())
.new Info(rootFolderResponse.getJsonObject());
System.out.println("Root Folder Created At: " + rootFolderInfo.getCreatedAt());
}
}
126 changes: 126 additions & 0 deletions src/main/java/com/box/sdk/BatchAPIRequest.java
@@ -0,0 +1,126 @@
package com.box.sdk;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.box.sdk.http.HttpHeaders;
import com.box.sdk.http.HttpMethod;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;

/**
* Used to make a bunch of HTTP Requests as a batch. Currently the number of requests that can be batched at once
* is capped at <b>20</b> by the API layer. Also there are certain requests which <b>cannot</b> be performed
* by Batch API. Check API documentation for more information.
*
* <p>The request itself is a BoxJSONRequest but extends it to provide additional functionality for aggregating
* a bunch of requests and responding with multiple responses as if the requests were called individually</p>
*/
public class BatchAPIRequest extends BoxJSONRequest {
/**
* Batch URL Template.
*/
public static final URLTemplate BATCH_URL_TEMPLATE = new URLTemplate("batch");
private final BoxAPIConnection api;

/**
* Constructs an authenticated BatchRequest using a provided BoxAPIConnection.
* @param api an API connection for authenticating the request.
*/
public BatchAPIRequest(BoxAPIConnection api) {
super(api, BATCH_URL_TEMPLATE.build(api.getBaseURL()), HttpMethod.GET);
this.api = api;
}

/**
* Execute a set of API calls as batch request.
* @param requests list of api requests that has to be executed in batch.
* @return list of BoxAPIResponses
*/
public List<BoxAPIResponse> execute(List<BoxAPIRequest> requests) {
this.prepareRequest(requests);
BoxJSONResponse batchResponse = (BoxJSONResponse) send();
return this.parseResponse(batchResponse);
}

/**
* Prepare a batch api request using list of individual reuests.
* @param requests list of api requests that has to be executed in batch.
*/
protected void prepareRequest(List<BoxAPIRequest> requests) {
JsonObject body = new JsonObject();
JsonArray requestsJSONArray = new JsonArray();
for (BoxAPIRequest request: requests) {
JsonObject batchRequest = new JsonObject();
batchRequest.add("method", request.getMethod());
batchRequest.add("relative_url", request.getUrl().toString().substring(this.api.getBaseURL().length() - 1));
//If the actual request has a JSON body then add it to vatch request
if (request instanceof BoxJSONRequest) {
BoxJSONRequest jsonRequest = (BoxJSONRequest) request;
batchRequest.add("body", jsonRequest.getBodyAsJsonObject());
}
//Add any headers that are in the request, except Authorization
if (request.getHeaders() != null) {
JsonObject batchRequestHeaders = new JsonObject();
for (RequestHeader header: request.getHeaders()) {
if (header.getKey() != null && !header.getKey().isEmpty()
&& HttpHeaders.AUTHORIZATION.equals(header.getKey())) {
batchRequestHeaders.add(header.getKey(), header.getValue());
}
}
batchRequest.add("headers", batchRequestHeaders);
}

//Add the request to array
requestsJSONArray.add(batchRequest);
}
//Add the requests array to body
body.add("requests", requestsJSONArray);
super.setBody(body);
}

/**
* Parses btch api response to create a list of BoxAPIResponse objects.
* @param batchResponse response of a batch api request
* @return list of BoxAPIResponses
*/
protected List<BoxAPIResponse> parseResponse(BoxJSONResponse batchResponse) {
JsonObject responseJSON = JsonObject.readFrom(batchResponse.getJSON());
List<BoxAPIResponse> responses = new ArrayList<BoxAPIResponse>();
Iterator<JsonValue> responseIterator = responseJSON.get("responses").asArray().iterator();
while (responseIterator.hasNext()) {
JsonObject jsonResponse = responseIterator.next().asObject();
BoxAPIResponse response = null;

//Gather headers
Map<String, String> responseHeaders = new HashMap<String, String>();

if (jsonResponse.get("headers") != null) {
JsonObject batchResponseHeadersObject = jsonResponse.get("headers").asObject();
for (JsonObject.Member member : batchResponseHeadersObject) {
String headerName = member.getName();
String headerValue = member.getValue().asString();
responseHeaders.put(headerName, headerValue);
}
}

// Construct a BoxAPIResponse when response is null, or a BoxJSONResponse when there's a response
// (not anticipating any other response as per current APIs.
// Ideally we should do it based on response header)
if (jsonResponse.get("response") == null) {
response =
new BoxAPIResponse(jsonResponse.get("status").asInt(), responseHeaders);
} else {
response =
new BoxJSONResponse(jsonResponse.get("status").asInt(), responseHeaders,
jsonResponse.get("response").asObject());
}
responses.add(response);
}
return responses;
}
}
53 changes: 48 additions & 5 deletions src/main/java/com/box/sdk/BoxAPIRequest.java
Expand Up @@ -14,6 +14,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import com.box.sdk.http.HttpHeaders;
import com.box.sdk.http.HttpMethod;

/**
Expand Down Expand Up @@ -81,11 +82,20 @@ public BoxAPIRequest(BoxAPIConnection api, URL url, String method) {
/**
* Constructs an authenticated BoxAPIRequest using a provided BoxAPIConnection.
* @param api an API connection for authenticating the request.
* @param uploadPartEndpoint the URL of the request.
* @param url the URL of the request.
* @param method the HTTP method of the request.
*/
public BoxAPIRequest(BoxAPIConnection api, URL uploadPartEndpoint, HttpMethod method) {
this(api, uploadPartEndpoint, method.name());
public BoxAPIRequest(BoxAPIConnection api, URL url, HttpMethod method) {
this(api, url, method.name());
}

/**
* Constructs an request, using URL and HttpMethod.
* @param url the URL of the request.
* @param method the HTTP method of the request.
*/
public BoxAPIRequest(URL url, HttpMethod method) {
this(url, method.name());
}

/**
Expand Down Expand Up @@ -182,6 +192,23 @@ public URL getUrl() {
return this.url;
}

/**
* Gets the http method from the request.
*
* @return http method
*/
public String getMethod() {
return this.method;
}

/**
* Get headers as list of RequestHeader objects.
* @return headers as list of RequestHeader objects
*/
protected List<RequestHeader> getHeaders() {
return this.headers;
}

/**
* Sends this request and returns a BoxAPIResponse containing the server's response.
*
Expand Down Expand Up @@ -377,7 +404,7 @@ private BoxAPIResponse trySend(ProgressListener listener) {

if (this.api != null) {
if (this.shouldAuthenticate) {
connection.addRequestProperty("Authorization", "Bearer " + this.api.lockAccessToken());
connection.addRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + this.api.lockAccessToken());
}
connection.setRequestProperty("User-Agent", this.api.getUserAgent());
if (this.api.getProxy() != null) {
Expand Down Expand Up @@ -533,19 +560,35 @@ private static boolean isResponseRedirect(int responseCode) {
return (responseCode == 301 || responseCode == 302);
}

private final class RequestHeader {
/**
* Class for mapping a request header and value.
*/
public final class RequestHeader {
private final String key;
private final String value;

/**
* Construct a request header from header key and value.
* @param key header name
* @param value header value
*/
public RequestHeader(String key, String value) {
this.key = key;
this.value = value;
}

/**
* Get header key.
* @return http header name
*/
public String getKey() {
return this.key;
}

/**
* Get header value.
* @return http header value
*/
public String getValue() {
return this.value;
}
Expand Down

0 comments on commit 744ad33

Please sign in to comment.