Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for updating event data instances with binary data. #6

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.coscale.sdk-java</groupId>
<artifactId>coscale-sdk-java</artifactId>
<version>1.1.0</version>
<version>1.2-beta-2</version>
<packaging>jar</packaging>
<name>CoScale SDK</name>
<description>Java SDK for integrating apps with CoScale Web Performance Monitoring platform.</description>
Expand Down Expand Up @@ -144,5 +144,10 @@
<artifactId>jsr305</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
</dependencies>
</project>
173 changes: 167 additions & 6 deletions src/main/java/com/coscale/sdk/client/ApiClient.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.coscale.sdk.client;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;

import com.coscale.sdk.client.commons.Options;
import com.coscale.sdk.client.data.BinaryData;
import com.coscale.sdk.client.exceptions.CoscaleApiException;
import com.coscale.sdk.client.utils.MapperSupport;
import com.fasterxml.jackson.core.JsonGenerationException;
Expand Down Expand Up @@ -66,6 +68,18 @@ public class ApiClient {
/** Json deserialization error counter. */
private int jsonDeserializationExceptions;

/** Field name in form upload for binary data. */
private String attachmentName = "bData";

/** CRLF. */
private String crlf = "\r\n";

/** Two hypens. */
private String twoHyphens = "--";

/** Boundary for binary file upload form. */
private String boundary = "*****";

/**
* ApiClient constructor.
* @param appId The CoScale Application id.
Expand Down Expand Up @@ -211,7 +225,7 @@ public int getDeserializationExceptions() {
* used by request.
* @param uri
* of the API call.
* @param data
* @param payload
* in string format to pass to the request.
* @return String response for the request.
* @throws IOException
Expand Down Expand Up @@ -301,7 +315,7 @@ private void login() throws IOException {
: getGlobalRequestURL("/users/login/");
Credentials.TokenHelper data = call("POST", uri, credentials,
new TypeReference<Credentials.TokenHelper>() {
}, false);
}, false, false);

token = data.token;
}
Expand Down Expand Up @@ -336,7 +350,53 @@ public <T> T callWithAuth(String method, String endpoint, Object obj, TypeRefere
login();
}
try {
return call(method, getAppRequestURL(endpoint), obj, valueType, true);
return call(method, getAppRequestURL(endpoint), obj, valueType, true, false);
} catch (CoscaleApiException e) {
if (e.statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
this.token = null; // will trigger new login
} else {
throw e;
}
}

tries++;
} while (tries <= AUTH_RETRIES);
throw new CoscaleApiException(responseCode, "Authentication failed.");
}

/**
* callWithAuthBinary is used to make requests that require Authentication on
* CoScale API.
*
* @param method
* request HTTP method.
* @param endpoint
* The url for the request.
* @param data
* object with data for the request. This parameter can be null.
* @param valueType
* is the type expected.
* @param binary
* is the action a binary upload.
* @return The Object received as a result for the request.
* @throws IOException
*/
public <T> T callWithAuthBinary(String method, String endpoint, BinaryData data, TypeReference<T> valueType,
boolean binary) throws IOException {
// Not authenticated yet, try login.
if (this.token == null) {
login();
}

// Do the actual request.
int tries = 0;
int responseCode = 0;
do {
if (this.token == null) {
login();
}
try {
return call(method, getAppRequestURL(endpoint), data, valueType, true, binary);
} catch (CoscaleApiException e) {
if (e.statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
this.token = null; // will trigger new login
Expand Down Expand Up @@ -365,13 +425,18 @@ public <T> T callWithAuth(String method, String endpoint, Object obj, TypeRefere
* @throws IOException
*/
public <T> T call(String method, String url, Object obj, TypeReference<T> valueType,
boolean auth) throws IOException {
boolean auth, boolean binary) throws IOException {
try {

String res = this.doHttpRequest(method, url, objectToJson(obj), auth);
String res;
if (!binary) {
res = this.doHttpRequest(method, url, objectToJson(obj), auth);
} else {
res = this.doHttpRequestBinary(method, url, (BinaryData) obj, auth);
}
if (res.length() == 0) {
return null;
}

return MapperSupport.getInstance().readValue(res, valueType);
} catch (JsonMappingException | JsonParseException e) {
jsonDeserializationExceptions++;
Expand All @@ -381,6 +446,102 @@ public <T> T call(String method, String url, Object obj, TypeReference<T> valueT
}
}

/**
* Do an HTTP request with a binary upload in it.
* @param method The method
* @param uri The url
* @param payload The object with the data
* @param authenticate The authentication token.
* @return String response for the request.
* @throws IOException
*/
public String doHttpRequestBinary(String method, String uri, BinaryData payload, boolean authenticate) throws IOException {
URL url;
HttpURLConnection conn = null;
int responseCode = -1;

try {
url = new URL(uri);
conn = (HttpURLConnection) url.openConnection();

// Set connection timeout.
conn.setConnectTimeout(this.apiConnTimeoutMS);
conn.setReadTimeout(this.apiReadTimeoutMS);

// Setup the connection.
conn.setDoOutput(true);
conn.setInstanceFollowRedirects(false);
conn.setRequestMethod(method);
conn.setRequestProperty("Accept", "application/json");

conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Cache-Control", "no-cache");

conn.setUseCaches(false);

// add request headers.
conn.setRequestProperty("User-Agent", this.userAgent);

if (authenticate) {
conn.setRequestProperty(AUTH_HEADER, this.token);
}

if (payload != null && ("POST".equals(method) || "PUT".equals(method))) {
conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + this.boundary);
conn.setRequestProperty("Content-Length", Integer.toString(payload.bData.length).trim());
conn.setRequestProperty("Content-Transfer-Encoding", "binary");
conn.setRequestProperty("Content-Disposition", "form-data; name=\"bData\";");
}

if (payload != null) {
DataOutputStream request = new DataOutputStream(conn.getOutputStream());

request.writeBytes(this.twoHyphens + this.boundary + this.crlf);
request.writeBytes("Content-Disposition: form-data; name=\"" +
this.attachmentName + "\";filename=\"" +
payload.filename + "\"" + this.crlf);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where the filename gets set for the upload.

request.writeBytes(this.crlf);

request.write(payload.bData);

request.writeBytes(this.crlf);
request.writeBytes(this.twoHyphens + this.boundary +
this.twoHyphens + this.crlf);

request.flush();
request.close();
}

// Check the response code.
responseCode = conn.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
String errorMessage = convertStreamToString(conn.getErrorStream());

throw new CoscaleApiException(responseCode, "Failed : HTTP error code : "
+ responseCode + " msg: " + conn.getResponseMessage() + " url: "
+ conn.getURL() + " method " + conn.getRequestMethod() + " error message "
+ errorMessage);
}

return convertStreamToString(conn.getInputStream());
} catch (IOException e) {
if (conn != null) {
// return also the response from the API
String errorMessage = convertStreamToString(conn.getErrorStream());
String message = e.getMessage();
if (errorMessage.length() > 0) {
message += " error " + errorMessage;
}
throw new CoscaleApiException(responseCode, message, e);
}
throw e;
} finally {
if (conn != null) {
conn.disconnect();
}
}
}

/**
* getAppRequestURL will construct the URL for a request using the end point
* provided. This method will be used to construct the URL for a specific
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/com/coscale/sdk/client/data/BinaryData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.coscale.sdk.client.data;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;

import java.util.Arrays;

import org.apache.commons.codec.binary.Base64;

/**
* For sending binary data to an existing event.
* @author kdegroot
*/
public class BinaryData {

/** The binary data. */
public byte[] bData;

/** The provided filename. */
public String filename;

public BinaryData(byte[] bData, String filename) {
this.bData = bData;
this.filename = filename;
}

public BinaryData() {
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("bData", new String(Base64.decodeBase64(bData))).add("filename", filename).toString();
}

@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final BinaryData other = (BinaryData) obj;

return (Arrays.equals(this.bData, other.bData) && this.filename.equals(other.filename));
}

@Override
public int hashCode() {
return Objects.hashCode(bData, filename);
}
}
6 changes: 3 additions & 3 deletions src/main/java/com/coscale/sdk/client/data/DataApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/**
* CoScale API client used to insert data.
*
*
* @author cristi
*
*/
Expand All @@ -19,7 +19,7 @@ public class DataApi {

/**
* DataApi contructor.
*
*
* @param api
* ApiClient.
*/
Expand All @@ -29,7 +29,7 @@ public DataApi(ApiClient api) {

/**
* Insert data into the data-store
*
*
* @param data
* @return Msg containing the api response.
* @throws IOException
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/coscale/sdk/client/events/EventsApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.coscale.sdk.client.ApiClient;
import com.coscale.sdk.client.commons.Msg;
import com.coscale.sdk.client.commons.Options;
import com.coscale.sdk.client.data.BinaryData;
import com.fasterxml.jackson.core.type.TypeReference;

/**
Expand Down Expand Up @@ -179,4 +180,17 @@ public Msg deleteData(Long eventId, Long dataId) throws IOException {
});
}

/**
* Update an existing event with binary data.
* @param eventId The event id.
* @param dataId The event data id.
* @param data The data to upload.
* @return Msg response message.
* @throws IOException
*/
public Msg uploadBinary(Long eventId, Long dataId, BinaryData data) throws IOException {
return api.callWithAuthBinary("PUT", "/events/" + eventId + "/data/" + dataId + "/binary/", data,
new TypeReference<Msg>() {
}, true);
}
}