diff --git a/pom.xml b/pom.xml index 2f1583e..28822c7 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.coscale.sdk-java coscale-sdk-java - 1.1.0 + 1.2-beta-2 jar CoScale SDK Java SDK for integrating apps with CoScale Web Performance Monitoring platform. @@ -144,5 +144,10 @@ jsr305 3.0.0 + + commons-codec + commons-codec + 1.10 + diff --git a/src/main/java/com/coscale/sdk/client/ApiClient.java b/src/main/java/com/coscale/sdk/client/ApiClient.java index a6fd273..00db4c1 100755 --- a/src/main/java/com/coscale/sdk/client/ApiClient.java +++ b/src/main/java/com/coscale/sdk/client/ApiClient.java @@ -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; @@ -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. @@ -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 @@ -301,7 +315,7 @@ private void login() throws IOException { : getGlobalRequestURL("/users/login/"); Credentials.TokenHelper data = call("POST", uri, credentials, new TypeReference() { - }, false); + }, false, false); token = data.token; } @@ -336,7 +350,53 @@ public 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 callWithAuthBinary(String method, String endpoint, BinaryData data, TypeReference 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 @@ -365,13 +425,18 @@ public T callWithAuth(String method, String endpoint, Object obj, TypeRefere * @throws IOException */ public T call(String method, String url, Object obj, TypeReference 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++; @@ -381,6 +446,102 @@ public T call(String method, String url, Object obj, TypeReference 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); + 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 diff --git a/src/main/java/com/coscale/sdk/client/data/BinaryData.java b/src/main/java/com/coscale/sdk/client/data/BinaryData.java new file mode 100644 index 0000000..ea6e29c --- /dev/null +++ b/src/main/java/com/coscale/sdk/client/data/BinaryData.java @@ -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); + } +} diff --git a/src/main/java/com/coscale/sdk/client/data/DataApi.java b/src/main/java/com/coscale/sdk/client/data/DataApi.java index b3d07af..23e04c6 100755 --- a/src/main/java/com/coscale/sdk/client/data/DataApi.java +++ b/src/main/java/com/coscale/sdk/client/data/DataApi.java @@ -8,7 +8,7 @@ /** * CoScale API client used to insert data. - * + * * @author cristi * */ @@ -19,7 +19,7 @@ public class DataApi { /** * DataApi contructor. - * + * * @param api * ApiClient. */ @@ -29,7 +29,7 @@ public DataApi(ApiClient api) { /** * Insert data into the data-store - * + * * @param data * @return Msg containing the api response. * @throws IOException diff --git a/src/main/java/com/coscale/sdk/client/events/EventsApi.java b/src/main/java/com/coscale/sdk/client/events/EventsApi.java index a8b58f4..d273194 100755 --- a/src/main/java/com/coscale/sdk/client/events/EventsApi.java +++ b/src/main/java/com/coscale/sdk/client/events/EventsApi.java @@ -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; /** @@ -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() { + }, true); + } }