Skip to content

Commit

Permalink
refactor: ProactiveConnect to use DynamicEndpoint (#489)
Browse files Browse the repository at this point in the history
* refactor: ProactiveConnect uses DynamicEndpoint

* Split up DynamicEndpoint.parseResponse

* Fix getAllRooms recursion logic

* Updated changelog

* Refactored DownloadListItems endpoint

* More refactoring

* Migrated ProactiveConnect endpoint tests

* Handle binary requests

* Deleted legacy ProactiveConnect endpoints

* 100% coverage on ProactiveConnect

* Bump version: v7.9.0 → v7.10.0

* Remove non-public API deprecated members

* Bumped Jackson version

* Updated changelog

* Migrated tests to JUnit 5

* Remove junit-vintage dependency
  • Loading branch information
SMadani authored Oct 20, 2023
1 parent d9bb181 commit 8cfcf59
Show file tree
Hide file tree
Showing 128 changed files with 1,696 additions and 3,796 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = False
current_version = v7.9.0
current_version = v7.10.0
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)(?P<build>\d+))?
serialize =
{major}.{minor}.{patch}-{release}{build}
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

# [7.10.0] - 2023-10-20
- Added more locales for Verify v2 and Meetings APIs
- Added `check_url` to `VerificationResponse` to support synchronous Silent Authentication
- Removed previously deprecated internal classes & methods
- Internal refactoring of Proactive Connect and Meetings API implementations
- Bumped Jackson version to 2.15.3
- Migrated all remaining tests to JUnit 5 and removed dependency on `vintage-engine`

# [7.9.0] - 2023-09-28
- Added `get-full-pricing` implementation of Pricing API in `AccountClient`
- Added master API key default overloads for secret management in Account API
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ See all of our SDKs and integrations on the [Vonage Developer portal](https://de

## Installation

Releases are published to [Maven Central](https://central.sonatype.com/artifact/com.vonage/client/7.9.0/snippets).
Releases are published to [Maven Central](https://central.sonatype.com/artifact/com.vonage/client/7.10.0/snippets).
Instructions for your build system can be found in the snippets section.
They're also available from [here](https://mvnrepository.com/artifact/com.vonage/client/7.9.0).
They're also available from [here](https://mvnrepository.com/artifact/com.vonage/client/7.10.0).
Release notes can be found in the [changelog](CHANGELOG.md).

### Build It Yourself
Expand Down
7 changes: 3 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {

group = "com.vonage"
archivesBaseName = "client"
version = "7.9.0"
version = "7.10.0"
sourceCompatibility = "1.8"
targetCompatibility = "1.8"

Expand All @@ -29,13 +29,12 @@ dependencies {
implementation 'commons-codec:commons-codec:1.16.0'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'org.apache.httpcomponents:httpmime:4.5.14'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.3'
implementation 'io.openapitools.jackson.dataformat:jackson-dataformat-hal:1.0.9'
implementation 'com.vonage:jwt:1.0.2'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
testImplementation 'org.junit.vintage:junit-vintage-engine:5.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
testImplementation 'org.mockito:mockito-inline:4.11.0'
testImplementation 'jakarta.servlet:jakarta.servlet-api:4.0.4'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,26 @@
*/
package com.vonage.client;

import java.io.IOException;

/**
* Internal interface for defining endpoints.
*
* @param <RequestT> The request type.
* @param <ResponseT> The response type.
* Indicates that a class used for request is to be serialised as binary data (e.g. for uploads).
*
* @deprecated Please use Will be removed in a future release. Please use {@link RestEndpoint}.
* @since 7.10.0
*/
@Deprecated
public interface Method<RequestT, ResponseT> {
ResponseT execute(RequestT request) throws IOException, VonageClientException;
}
public interface BinaryRequest {

/**
* Serialises this request to a byte array.
*
* @return The binary data for this request.
*/
byte[] toByteArray();

/**
* The MIME type header for this request to use as the {@code Content-Type}.
*
* @return The request MIME type as a string.
*/
default String getContentType() {
return "multipart/form-data";
}
}
227 changes: 125 additions & 102 deletions src/main/java/com/vonage/client/DynamicEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Consumer;

Expand Down Expand Up @@ -112,13 +114,6 @@ public Builder<T, R> pathGetter(BiFunction<DynamicEndpoint<T, R>, T, String> pat
return this;
}

public Builder<T, R> addAuthMethodIfTrue(boolean condition, Class<? extends AuthMethod> primary, Class<? extends AuthMethod>... others) {
if (condition) {
authMethod(primary, others);
}
return this;
}

public Builder<T, R> authMethod(Class<? extends AuthMethod> primary, Class<? extends AuthMethod>... others) {
authMethods = new ArrayList<>(2);
authMethods.add(Objects.requireNonNull(primary, "Primary auth method cannot be null"));
Expand Down Expand Up @@ -191,14 +186,25 @@ else if (applyBasicAuth) {
}
}

private String getRequestHeader(T requestBody) {
if (contentType != null)
return contentType;
else if (requestBody instanceof Jsonable)
return "application/json";
else if (requestBody instanceof BinaryRequest)
return ((BinaryRequest) requestBody).getContentType();
else return null;
}

@Override
public final RequestBuilder makeRequest(T requestBody) throws UnsupportedEncodingException {
public final RequestBuilder makeRequest(T requestBody) {
if (requestBody instanceof Jsonable && requestBody.getClass().equals(responseType)) {
cachedRequestBody = requestBody;
}
RequestBuilder rqb = createRequestBuilderFromRequestMethod(requestMethod);
if (contentType != null || requestBody instanceof Jsonable) {
rqb.setHeader("Content-Type", contentType != null ? contentType : "application/json");
String header = getRequestHeader(requestBody);
if (header != null) {
rqb.setHeader("Content-Type", header);
}
if (accept != null) {
rqb.setHeader("Accept", accept);
Expand All @@ -225,128 +231,145 @@ else if (v instanceof Iterable<?>) {
if (requestBody instanceof Jsonable) {
rqb.setEntity(new StringEntity(((Jsonable) requestBody).toJson(), ContentType.APPLICATION_JSON));
}
else if (requestBody instanceof BinaryRequest) {
BinaryRequest bin = (BinaryRequest) requestBody;
rqb.setEntity(new ByteArrayEntity(bin.toByteArray(), ContentType.getByMimeType(bin.getContentType())));
}
else if (requestBody instanceof byte[]) {
rqb.setEntity(new ByteArrayEntity((byte[]) requestBody));
}
return rqb.setUri(pathGetter.apply(this, requestBody));
}

protected R parseResponseFromString(String response) {
return null;
}

@Override
public final R parseResponse(HttpResponse response) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
try {
if (statusCode >= 200 && statusCode < 300) {
if (responseType.equals(Void.class)) {
return null;
}
else if (byte[].class.equals(responseType)) {
return (R) EntityUtils.toByteArray(response.getEntity());
}
else {
String deser = basicResponseHandler.handleResponse(response);
return parseResponseSuccess(response);
}
else {
return parseResponseFailure(response);
}
}
catch (InvocationTargetException ex) {
Throwable wrapped = ex.getTargetException();
if (wrapped instanceof RuntimeException) {
throw (RuntimeException) wrapped;
}
else {
throw new VonageUnexpectedException(wrapped);
}
}
catch (ReflectiveOperationException ex) {
throw new VonageUnexpectedException(ex);
}
finally {
cachedRequestBody = null;
}
}

if (responseType.equals(String.class)) {
return (R) deser;
}
protected R parseResponseFromString(String response) {
return null;
}

if (cachedRequestBody instanceof Jsonable) {
((Jsonable) cachedRequestBody).updateFromJson(deser);
return (R) cachedRequestBody;
}
private R parseResponseSuccess(HttpResponse response) throws IOException, ReflectiveOperationException {
if (responseType == null || responseType.equals(Void.class)) {
return null;
}
else if (byte[].class.equals(responseType)) {
return (R) EntityUtils.toByteArray(response.getEntity());
}
else {
String deser = basicResponseHandler.handleResponse(response);

for (java.lang.reflect.Method method : responseType.getDeclaredMethods()) {
boolean matching = Modifier.isStatic(method.getModifiers()) &&
method.getName().equals("fromJson") &&
responseType.isAssignableFrom(method.getReturnType());
if (matching) {
Class<?>[] params = method.getParameterTypes();
if (params.length == 1 && params[0].equals(String.class)) {
if (!method.isAccessible()) {
method.setAccessible(true);
}
return (R) method.invoke(responseType, deser);
}
}
}
if (responseType.equals(String.class)) {
return (R) deser;
}

if (Jsonable.class.isAssignableFrom(responseType)) {
Constructor<R> constructor = responseType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
R responseBody = constructor.newInstance();
((Jsonable) responseBody).updateFromJson(deser);
return responseBody;
}
else if (Collection.class.isAssignableFrom(responseType)) {
return Jsonable.createDefaultObjectMapper().readValue(deser, responseType);
}
else {
R customParsedResponse = parseResponseFromString(deser);
if (customParsedResponse == null) {
throw new IllegalStateException("Unhandled return type: " + responseType);
}
else {
return customParsedResponse;
if (cachedRequestBody instanceof Jsonable) {
((Jsonable) cachedRequestBody).updateFromJson(deser);
return (R) cachedRequestBody;
}

for (java.lang.reflect.Method method : responseType.getDeclaredMethods()) {
boolean matching = Modifier.isStatic(method.getModifiers()) &&
method.getName().equals("fromJson") &&
responseType.isAssignableFrom(method.getReturnType());
if (matching) {
Class<?>[] params = method.getParameterTypes();
if (params.length == 1 && params[0].equals(String.class)) {
if (!method.isAccessible()) {
method.setAccessible(true);
}
return (R) method.invoke(responseType, deser);
}
}
}
else {
String exMessage = EntityUtils.toString(response.getEntity());
if (responseExceptionType != null) {
if (VonageApiResponseException.class.isAssignableFrom(responseExceptionType)) {
Constructor<? extends Exception> constructor = responseExceptionType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
VonageApiResponseException varex = (VonageApiResponseException) constructor.newInstance();
varex.updateFromJson(exMessage);
if (varex.title == null) {
varex.title = response.getStatusLine().getReasonPhrase();
}
varex.statusCode = response.getStatusLine().getStatusCode();
throw varex;
}
else {
for (Constructor<?> constructor : responseExceptionType.getDeclaredConstructors()) {
Class<?>[] params = constructor.getParameterTypes();
if (params.length == 1 && String.class.equals(params[0])) {
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
throw (RuntimeException) constructor.newInstance(exMessage);
}
}
}

if (Jsonable.class.isAssignableFrom(responseType)) {
Constructor<R> constructor = responseType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
R customParsedResponse = parseResponseFromString(exMessage);
R responseBody = constructor.newInstance();
((Jsonable) responseBody).updateFromJson(deser);
return responseBody;
}
else if (Collection.class.isAssignableFrom(responseType)) {
return Jsonable.createDefaultObjectMapper().readValue(deser, responseType);
}
else {
R customParsedResponse = parseResponseFromString(deser);
if (customParsedResponse == null) {
throw new VonageApiResponseException(exMessage);
throw new IllegalStateException("Unhandled return type: " + responseType);
}
else {
return customParsedResponse;
}
}
}
catch (InvocationTargetException ex) {
Throwable wrapped = ex.getTargetException();
if (wrapped instanceof RuntimeException) {
throw (RuntimeException) wrapped;
}

private R parseResponseFailure(HttpResponse response) throws IOException, ReflectiveOperationException {
String exMessage = EntityUtils.toString(response.getEntity());
if (responseExceptionType != null) {
if (VonageApiResponseException.class.isAssignableFrom(responseExceptionType)) {
Constructor<? extends Exception> constructor = responseExceptionType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
VonageApiResponseException varex = (VonageApiResponseException) constructor.newInstance();
try {
varex.updateFromJson(exMessage);
}
catch (VonageResponseParseException ex) {
throw new VonageUnexpectedException(exMessage);
}
if (varex.title == null) {
varex.title = response.getStatusLine().getReasonPhrase();
}
varex.statusCode = response.getStatusLine().getStatusCode();
throw varex;
}
else {
throw new VonageUnexpectedException(wrapped);
for (Constructor<?> constructor : responseExceptionType.getDeclaredConstructors()) {
Class<?>[] params = constructor.getParameterTypes();
if (params.length == 1 && String.class.equals(params[0])) {
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
throw (RuntimeException) constructor.newInstance(exMessage);
}
}
}
}
catch (InstantiationException | IllegalAccessException | NoSuchMethodException ex) {
throw new VonageUnexpectedException(ex);
R customParsedResponse = parseResponseFromString(exMessage);
if (customParsedResponse == null) {
throw new VonageApiResponseException(exMessage);
}
finally {
cachedRequestBody = null;
else {
return customParsedResponse;
}
}
}
Loading

0 comments on commit 8cfcf59

Please sign in to comment.