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

refactor: ProactiveConnect to use DynamicEndpoint #489

Merged
merged 16 commits into from
Oct 20, 2023
Merged
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
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