diff --git a/README.md b/README.md index 80472995..1acc7518 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The recommended method for obtaining the SDK is via Gradle or Maven through the ### Gradle ```groovy -compile "com.smartcar.sdk:java-sdk:3.6.0" +compile "com.smartcar.sdk:java-sdk:3.7.0" ``` ### Maven @@ -18,16 +18,16 @@ compile "com.smartcar.sdk:java-sdk:3.6.0" com.smartcar.sdk java-sdk - 3.6.0 + 3.7.0 ``` ### Jar Direct Download -* [java-sdk-3.6.0.jar](https://search.maven.org/remotecontent?filepath=com/smartcar/sdk/java-sdk/3.6.0/java-sdk-3.6.0.jar) -* [java-sdk-3.6.0-sources.jar](https://search.maven.org/remotecontent?filepath=com/smartcar/sdk/java-sdk/3.6.0/java-sdk-3.6.0-sources.jar) -* [java-sdk-3.6.0-javadoc.jar](https://search.maven.org/remotecontent?filepath=com/smartcar/sdk/java-sdk/3.6.0/java-sdk-3.6.0-javadoc.jar) +* [java-sdk-3.7.0.jar](https://search.maven.org/remotecontent?filepath=com/smartcar/sdk/java-sdk/3.7.0/java-sdk-3.7.0.jar) +* [java-sdk-3.7.0-sources.jar](https://search.maven.org/remotecontent?filepath=com/smartcar/sdk/java-sdk/3.7.0/java-sdk-3.7.0-sources.jar) +* [java-sdk-3.7.0-javadoc.jar](https://search.maven.org/remotecontent?filepath=com/smartcar/sdk/java-sdk/3.7.0/java-sdk-3.7.0-javadoc.jar) -Signatures and other downloads available at [Maven Central](https://search.maven.org/artifact/com.smartcar.sdk/java-sdk/3.6.0/jar). +Signatures and other downloads available at [Maven Central](https://search.maven.org/artifact/com.smartcar.sdk/java-sdk/3.7.0/jar). ## Usage @@ -136,5 +136,5 @@ In accordance with the Semantic Versioning specification, the addition of suppor [ci-url]: https://travis-ci.com/smartcar/java-sdk [coverage-image]: https://codecov.io/gh/smartcar/java-sdk/branch/master/graph/badge.svg?token=nZAITx7w3X [coverage-url]: https://codecov.io/gh/smartcar/java-sdk -[javadoc-image]: https://img.shields.io/badge/javadoc-3.6.0-brightgreen.svg +[javadoc-image]: https://img.shields.io/badge/javadoc-3.7.0-brightgreen.svg [javadoc-url]: https://smartcar.github.io/java-sdk diff --git a/gradle.properties b/gradle.properties index ec61dc22..0130598b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ libGroup=com.smartcar.sdk libName=java-sdk -libVersion=3.6.0 +libVersion=3.7.0 libDescription=Smartcar Java SDK diff --git a/src/integration/java/com/smartcar/sdk/SmartcarTest.java b/src/integration/java/com/smartcar/sdk/SmartcarTest.java index 0df001e3..d66f827d 100644 --- a/src/integration/java/com/smartcar/sdk/SmartcarTest.java +++ b/src/integration/java/com/smartcar/sdk/SmartcarTest.java @@ -1,9 +1,6 @@ package com.smartcar.sdk; -import com.smartcar.sdk.data.Compatibility; -import com.smartcar.sdk.data.RequestPaging; -import com.smartcar.sdk.data.User; -import com.smartcar.sdk.data.VehicleIds; +import com.smartcar.sdk.data.*; import com.smartcar.sdk.helpers.AuthHelpers; import org.testng.Assert; import org.testng.annotations.BeforeSuite; @@ -14,8 +11,10 @@ public class SmartcarTest { private String accessToken; + private VehicleIds vehicleIds; private String clientId = System.getenv("E2E_SMARTCAR_CLIENT_ID"); private String clientSecret = System.getenv("E2E_SMARTCAR_CLIENT_SECRET"); + private String amt = System.getenv("E2E_SMARTCAR_AMT"); private AuthClient client; private String authorizeUrl; private String[] scope = {"read_odometer"}; @@ -26,6 +25,8 @@ public void beforeSuite() throws Exception { this.authorizeUrl = client.authUrlBuilder(new String[]{"read_vehicle_info"}).build(); String code = AuthHelpers.runAuthFlow(client.authUrlBuilder(this.scope).build()); this.accessToken = client.exchangeCode(code).getAccessToken(); + VehicleIds vehicleIds = Smartcar.getVehicles(this.accessToken); + this.vehicleIds = vehicleIds; } @Test @@ -78,4 +79,26 @@ public void testGetCompatibility() throws Exception { } Assert.assertFalse((capable)); } + + @Test + public void testGetConnections() throws SmartcarException { + String testVehicleId = this.vehicleIds.getVehicleIds()[0]; + ConnectionsFilter filter = new ConnectionsFilter + .Builder() + .vehicleId(testVehicleId) + .build(); + GetConnections connections = Smartcar.getConnections(this.amt, filter); + Assert.assertEquals(connections.getConnections().length, 1); + } + + @Test + public void testDeleteConnections() throws SmartcarException { + String testVehicleId = this.vehicleIds.getVehicleIds()[0]; + ConnectionsFilter filter = new ConnectionsFilter + .Builder() + .vehicleId(testVehicleId) + .build(); + DeleteConnections connections = Smartcar.deleteConnections(this.amt, filter); + Assert.assertEquals(connections.getConnections().length, 1); + } } diff --git a/src/main/java/com/smartcar/sdk/Smartcar.java b/src/main/java/com/smartcar/sdk/Smartcar.java index 7c19618e..98b59cd8 100644 --- a/src/main/java/com/smartcar/sdk/Smartcar.java +++ b/src/main/java/com/smartcar/sdk/Smartcar.java @@ -12,7 +12,8 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import javax.json.JsonObject; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -20,6 +21,7 @@ public class Smartcar { public static String API_VERSION = "2.0"; public static String API_ORIGIN = "https://api.smartcar.com"; + public static String MANAGEMENT_API_ORIGIN = "https://management.smartcar.com"; /** * Sets the Smartcar API version @@ -40,7 +42,6 @@ static String getApiUrl() { } /** - * * @return Smartcar API origin */ static String getApiOrigin() { @@ -51,6 +52,17 @@ static String getApiOrigin() { return apiOrigin; } + /** + * @return Smartcar Management API origin + */ + static String getManagementApiOrigin() { + String managementApiOrigin = System.getenv("SMARTCAR_MANAGEMENT_API_ORIGIN"); + if (managementApiOrigin == null) { + return MANAGEMENT_API_ORIGIN; + } + return managementApiOrigin; + } + /** * Retrieves the user ID of the user authenticated with the specified access token. * @@ -72,7 +84,7 @@ public static User getUser(String accessToken) throws SmartcarException { * Retrieves all vehicles associated with the authenticated user. * * @param accessToken a valid access token - * @param paging paging parameters + * @param paging paging parameters * @return the requested vehicle IDs * @throws SmartcarException if the request is unsuccessful */ @@ -111,7 +123,7 @@ public static VehicleIds getVehicles(String accessToken) * Convenience method for determining if an auth token expiration has passed. * * @param expiration the expiration date of the token - * @return whether or not the token has expired + * @return whether the token has expired */ public static boolean isExpired(Date expiration) { return !expiration.after(new Date()); @@ -129,7 +141,7 @@ public static boolean isExpired(Date expiration) { * * @param compatibilityRequest with options for this request. See Smartcar.SmartcarCompatibilityRequest * @return A Compatibility object with isCompatible set to false if the vehicle is not compatible in the specified country and true if the vehicle is - * likely compatible. + * likely compatible. * @throws SmartcarException when the request is unsuccessful */ public static Compatibility getCompatibility(SmartcarCompatibilityRequest compatibilityRequest) throws SmartcarException { @@ -144,7 +156,6 @@ public static Compatibility getCompatibility(SmartcarCompatibilityRequest compat .addQueryParameter("country", compatibilityRequest.getCountry()); - if (compatibilityRequest.getFlags() != null) { urlBuilder.addQueryParameter("flags", compatibilityRequest.getFlags()); } @@ -168,6 +179,7 @@ public static Compatibility getCompatibility(SmartcarCompatibilityRequest compat /** * Performs a HmacSHA256 hash on a challenge string using the key provided + * * @param key * @param challenge * @return String digest @@ -186,13 +198,146 @@ public static String hashChallenge(String key, String challenge) throws Smartcar /** * Verifies as HmacSHA256 signature + * * @param applicationManagementToken * @param signature * @param payload - * @return boolean whether or not the signature was verified + * @return boolean whether the signature was verified * @throws SmartcarException */ public static boolean verifyPayload(String applicationManagementToken, String signature, String payload) throws SmartcarException { return Smartcar.hashChallenge(applicationManagementToken, payload).equals(signature); } + + /** + * Returns a paged list of all the vehicles that are connected to the application associated + * with the management API token used sorted in descending order by connection date. + * + * @param applicationManagementToken + * @param filter + * @param paging + * @return connections + * @throws SmartcarException if the request is unsuccessful + */ + public static GetConnections getConnections(String applicationManagementToken, ConnectionsFilter filter, RequestPagingCursor paging) + throws SmartcarException { + // Build Request + HttpUrl.Builder urlBuilder = HttpUrl + .parse(Smartcar.getManagementApiOrigin() + "/v" + Smartcar.API_VERSION + "/management/connections") + .newBuilder(); + + if (filter != null) { + if (filter.getUserId() != null) { + urlBuilder + .addQueryParameter("user_id", String.valueOf(filter.getUserId())); + } + if (filter.getVehicleId() != null) { + urlBuilder + .addQueryParameter("vehicle_id", String.valueOf(filter.getVehicleId())); + } + } + if (paging != null) { + if (paging.getCursor() != null) { + urlBuilder + .addQueryParameter("cursor", String.valueOf(paging.getCursor())); + } + if (paging.getLimit() != null) { + urlBuilder + .addQueryParameter("limit", String.valueOf(paging.getLimit())); + } + } + + HttpUrl url = urlBuilder.build(); + Map headers = new HashMap<>(); + headers.put("Authorization", Credentials.basic( + "default", + applicationManagementToken + )); + Request request = ApiClient.buildRequest(url, "GET", null, headers); + + return ApiClient.execute(request, GetConnections.class); + } + + /** + * Returns a paged list of all the vehicles that are connected to the application associated + * with the management API token used sorted in descending order by connection date. + * + * @param applicationManagementToken + * @param filter + * @throws SmartcarException if the request is unsuccessful + */ + public static GetConnections getConnections(String applicationManagementToken, ConnectionsFilter filter) + throws SmartcarException { + return Smartcar.getConnections(applicationManagementToken, filter, null); + } + + /** + * Returns a paged list of all the vehicles that are connected to the application associated + * with the management API token used sorted in descending order by connection date. + * + * @param applicationManagementToken + * @throws SmartcarException if the request is unsuccessful + */ + public static GetConnections getConnections(String applicationManagementToken) + throws SmartcarException { + return Smartcar.getConnections(applicationManagementToken, null, null); + } + + /** + * Deletes all the connections by vehicle or user ID and returns a list of all connections that were deleted. + * + * @param applicationManagementToken + * @param filter + * @return connections + * @throws SmartcarException if the request is unsuccessful + */ + public static DeleteConnections deleteConnections(String applicationManagementToken, ConnectionsFilter filter) + throws SmartcarException { + // Build Request + HttpUrl.Builder urlBuilder = HttpUrl + .parse(Smartcar.getManagementApiOrigin() + "/v" + Smartcar.API_VERSION + "/management/connections") + .newBuilder(); + + if (filter != null) { + String userId = filter.getUserId(); + String vehicleId = filter.getVehicleId(); + if (userId != null && vehicleId != null) { + throw new SmartcarException + .Builder() + .type("SDK_ERROR") + .description("Filter can contain EITHER user_id OR vehicle_id, not both") + .build(); + } + if (userId != null) { + urlBuilder + .addQueryParameter("user_id", String.valueOf(userId)); + } + if (vehicleId != null) { + urlBuilder + .addQueryParameter("vehicle_id", String.valueOf(vehicleId)); + } + } + + HttpUrl url = urlBuilder.build(); + Map headers = new HashMap<>(); + headers.put("Authorization", Credentials.basic( + "default", + applicationManagementToken + )); + Request request = ApiClient.buildRequest(url, "GET", null, headers); + + return ApiClient.execute(request, DeleteConnections.class); + } + + private static String getManagementToken(String applicationManagementToken, String username) { + String credentials = username + ":" + applicationManagementToken; + byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(credentialsBytes); + } + + private static String getManagementToken(String applicationManagementToken) { + String credentials = "default:" + applicationManagementToken; + byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(credentialsBytes); + } } diff --git a/src/main/java/com/smartcar/sdk/data/Connection.java b/src/main/java/com/smartcar/sdk/data/Connection.java new file mode 100644 index 00000000..a2713f98 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/Connection.java @@ -0,0 +1,28 @@ +package com.smartcar.sdk.data; + +public class Connection { + private String vehicleId; + private String userId; + private String connectedAt; + + public String getVehicleId() { + return vehicleId; + } + + public String getUserId() { + return userId; + } + + public String getConnectedAt() { + return connectedAt; + } + + @Override + public String toString() { + return "Connection{" + + "vehicleId='" + vehicleId + '\'' + + ", userId='" + userId + '\'' + + ", createdAt='" + connectedAt + '\'' + + '}'; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/ConnectionsFilter.java b/src/main/java/com/smartcar/sdk/data/ConnectionsFilter.java new file mode 100644 index 00000000..7100b601 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/ConnectionsFilter.java @@ -0,0 +1,46 @@ +package com.smartcar.sdk.data; + +public class ConnectionsFilter { + private String userId; + private String vehicleId; + + private ConnectionsFilter(Builder builder) { + this.userId = builder.userId; + this.vehicleId = builder.vehicleId; + } + + public String getUserId() { + return userId; + } + + public String getVehicleId() { + return vehicleId; + } + + public static class Builder { + private String userId; + private String vehicleId; + + public ConnectionsFilter.Builder userId(String userId) { + this.userId = userId; + return this; + } + + public ConnectionsFilter.Builder vehicleId(String vehicleId) { + this.vehicleId = vehicleId; + return this; + } + + public ConnectionsFilter build() { + return new ConnectionsFilter(this); + } + } + + @Override + public String toString() { + return "ConnectionsFilter{" + + "userId='" + userId + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/DeleteConnections.java b/src/main/java/com/smartcar/sdk/data/DeleteConnections.java new file mode 100644 index 00000000..9b28f356 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/DeleteConnections.java @@ -0,0 +1,21 @@ +package com.smartcar.sdk.data; + +import java.util.Arrays; + +public class DeleteConnections extends ApiData { + private Connection[] connections; + + public DeleteConnections(final Connection[] connections) { + this.connections = connections; + } + + public Connection[] getConnections() { + return connections; + } + + @Override public String toString() { + return "DeleteConnections{" + + "connections=" + Arrays.toString(connections) + + '}'; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/GetConnections.java b/src/main/java/com/smartcar/sdk/data/GetConnections.java new file mode 100644 index 00000000..1d69ad58 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/GetConnections.java @@ -0,0 +1,29 @@ +package com.smartcar.sdk.data; + +import java.util.Arrays; + +public class GetConnections extends ApiData { + private Connection[] connections; + + private ResponsePagingCursor paging; + public GetConnections(final Connection[] connections, final ResponsePagingCursor paging) { + this.connections = connections; + this.paging = paging; + } + + public Connection[] getConnections() { + return connections; + } + + public ResponsePagingCursor getPaging() { + return paging; + } + + @Override + public String toString() { + return "GetConnections{" + + "connections=" + Arrays.toString(connections) + + ", paging=" + paging + + '}'; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/RequestPagingCursor.java b/src/main/java/com/smartcar/sdk/data/RequestPagingCursor.java new file mode 100644 index 00000000..805d5103 --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/RequestPagingCursor.java @@ -0,0 +1,47 @@ +package com.smartcar.sdk.data; + +/** Builder class for setting request paging w/cursor options */ +public class RequestPagingCursor { + private Integer limit; + private String cursor; + + private RequestPagingCursor(Builder builder) { + this.limit = builder.limit; + this.cursor = builder.cursor; + } + + public Integer getLimit() { + return limit; + } + + public String getCursor() { + return cursor; + } + + public static class Builder { + private Integer limit; + private String cursor; + + public Builder limit(Integer limit) { + this.limit = limit; + return this; + } + + public Builder cursor(String cursor) { + this.cursor = cursor; + return this; + } + + public RequestPagingCursor build() { + return new RequestPagingCursor(this); + } + } + + @Override + public String toString() { + return "RequestPagingCursor{" + + "limit=" + limit + + ", cursor='" + cursor + '\'' + + '}'; + } +} diff --git a/src/main/java/com/smartcar/sdk/data/ResponsePagingCursor.java b/src/main/java/com/smartcar/sdk/data/ResponsePagingCursor.java new file mode 100644 index 00000000..f91cca9b --- /dev/null +++ b/src/main/java/com/smartcar/sdk/data/ResponsePagingCursor.java @@ -0,0 +1,21 @@ +package com.smartcar.sdk.data; + +/** POJO for the response paging w/cursor object */ +public class ResponsePagingCursor extends ApiData { + private String cursor; + + /** + * Returns the response cursor + * + * @return response cursor + */ + public String getCursor() { + return this.cursor; + } + + /** @return a stringified representation of ResponsePagingCursor */ + @Override + public String toString() { + return this.getClass().getName() + "{" + "cursor=" + cursor + '}'; + } +} diff --git a/src/test/resources/DeleteConnections.json b/src/test/resources/DeleteConnections.json new file mode 100644 index 00000000..22685059 --- /dev/null +++ b/src/test/resources/DeleteConnections.json @@ -0,0 +1,8 @@ +{ + "connections": [ + { + "vehicleId": "bcf1b859-4bd1-41c5-80bc-b8abfe3623fc", + "userId": "d2f64075-d0e1-45d4-aee0-72bd39088947" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/GetConnections.json b/src/test/resources/GetConnections.json new file mode 100644 index 00000000..3127ff44 --- /dev/null +++ b/src/test/resources/GetConnections.json @@ -0,0 +1,13 @@ +{ + "connections": [ + { + "connectedAt": "2023-08-10T15:00:00.000Z", + "vehicleId": "bcf1b859-4bd1-41c5-80bc-b8abfe3623fc", + "userId": "d2f64075-d0e1-45d4-aee0-72bd39088947" + } + ], + "paging": { + "cursor": "AES-256-CBC::base64::utf8::zLBjfq8xCCTKr3grfBFNXg==Oh0pX00AHjxsloC6vHMyKw==", + "count": 1 + } +} \ No newline at end of file