Skip to content

Commit

Permalink
feat: Add Conversations v1 (#518)
Browse files Browse the repository at this point in the history
* Extract HalRequestFilter to common

* Add Conversations classes

* Validate IDs

* Tidy up requests

* Add BaseConversation

* Further refinement of Conversation

* Add ListUserConversationsResponse

* Fix failing tests

* Tidy up UserSession req/res

* Tidy up List*Request classes

* Add Member fields

* Member media classes & fix tests

* Add MemberTimestamp

* assertEqualsSampleMember

* Implement MemberInitiator

* More progress on tests

* Fix testListMembers

* Fix Callback.Params not being assigned

* Mandatory parameter validation for Member

* String length validation

* First attempt at Channel

* Another attempt at Channel inference

* Custom deserialiser now working

* Update Member.Builder with Channel options

* More MemberChannel serdes fixes

* Add Channel.removeTypeField

* MemberMedia builder & testCreateMemberEndpoint

* Remove ConversationsClient.validateSessionId

* Remove unnecessary fromJson methods

* Test Callback.Params.applicationId

* Remove CallbackHttpMethod for simplicity

* 100% conversations coverage

* Coverage for channel deserialisation

* Placeholder ConversationClient javadocs

* Factor out pageSize in ConversationsClient

* Return List<BaseConversation> from listConversations

* Add cursor to List*Request

* More documentation

* Make State mandatory in UpdateMemberRequest

* Validate UpdateMemberRequest params

* Bump version: v8.3.0 → v8.4.0

* Bump Gradle to 8.6

* Bump JWT library version

* Bump Jackson version

* Add MessageType to common

* Add Event endpoints

* Documentation for Event endpoints

* Make Event abstract

* Generic Event body

* Abstract GenericEvent

* Update member invite doc

* Message event classes

* Remove ListUserSessions

* Internalise Event body generic

* Further improvement to Event.Builder

* More work on Events

* Add MessageEvent

* Add MessageEvent.Builder fields

* Documentation & validation for events

* Fix failing test

* Add AudioRecord fields

* Documented AudioRecordEvent

* Add AudioSayEvent fields

* Add all event type enums

* Add Get/Delete event tests

* Bump Gradle to 8.7

* Fix Event parsing

* Don't look for fromJson in DynamicEndpoint

* Simpify exception construction in DynamicEndpoint

* Assertions for sample event

* Test ListEvents

* Remove unnecessary fromJson

* Finish / fix Event endpoint tests

* Readd missing Gradle wrapper

* Add JsonIgnore for Say events

* Remove UserSession

* StatusEvent tests

* Test event body JSON

* Ordered Map for JSON tests

* Test base event type

* Test AudioPlayEvent

* Test AudioSayEvent

* Test remaining read-only events

* Test MessageEvent

* 100% Conversations coverage

* Only support Phone channel

* Validate AudioOutEvent bounds

* Fix failing tests
  • Loading branch information
SMadani authored Apr 5, 2024
1 parent d41ba0e commit 5f6687d
Show file tree
Hide file tree
Showing 144 changed files with 9,719 additions and 274 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 = v8.3.0
current_version = v8.4.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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ 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/).

# [8.4.0] - 2024-04-05
- Added Conversation API implementation
- Type inference for User channels (`com.vonage.client.users.channels`)
- Added `com.vonage.client.common.ChannelType` enum
- Added `@JsonSubTypes` for improved deserialisation support
- Added `type` field to `Channel`
- Methods `setTypeField()` and `removeTypeField()` for automatically setting / unsetting based on the class
- New class `com.vonage.client.common.HalFilterRequest`
- Added `com.vonage.client.common.SortOrder`
- Used for grouping common "List*Request" fields
- Bumped Jackson version to 2.17.0
- Bumped JWT library version to 1.1.1

# [8.3.0] - 2024-02-12
- Made `from` parameter mandatory in Verify v2 WhatsApp workflows
- Added `com.vonage.client.sms.MessageEvent` for SMS webhooks
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ need a Vonage account. You'll need to have [created a Vonage account](https://da

- [Account](https://developer.vonage.com/en/account/overview)
- [Application](https://developer.vonage.com/en/application/overview)
- [Conversation](https://developer.vonage.com/conversation/overview)
- [Conversion](https://developer.vonage.com/messaging/conversion-api/overview)
- [Meetings](https://developer.vonage.com/en/meetings/overview)
- [Messages](https://developer.vonage.com/en/messages/overview)
Expand Down Expand Up @@ -51,9 +52,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/server-sdk/8.3.0/snippets).
Releases are published to [Maven Central](https://central.sonatype.com/artifact/com.vonage/server-sdk/8.4.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/server-sdk/8.3.0).
They're also available from [here](https://mvnrepository.com/artifact/com.vonage/server-sdk/8.4.0).
Release notes can be found in the [changelog](CHANGELOG.md).

### Build It Yourself
Expand Down
38 changes: 20 additions & 18 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,16 @@ plugins {

group = "com.vonage"
archivesBaseName = "server-sdk"
version = "8.3.0"
version = "8.4.0"

ext.githubPath = 'Vonage/vonage-java-sdk'

tasks.withType(Javadoc).configureEach {
options.addStringOption('Xdoclint:all', '-quiet')
}

repositories {
mavenCentral()
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = sourceCompatibility
}

compileTestJava {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = sourceCompatibility
}

dependencies {
def jacksonVersion = '2.16.1'
def jacksonVersion = '2.17.0'
def httpclientVersion = '4.5.14'
def junitVersion = '5.10.2'

Expand All @@ -42,21 +28,37 @@ dependencies {
implementation "org.apache.httpcomponents:httpmime:$httpclientVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion"
implementation "com.vonage:jwt:1.1.0"
implementation "com.vonage:jwt:1.1.1"

testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testImplementation "org.mockito:mockito-core:5.10.0"
testImplementation "org.mockito:mockito-core:5.11.0"
testImplementation "jakarta.servlet:jakarta.servlet-api:4.0.4"
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = sourceCompatibility
}

compileTestJava {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = sourceCompatibility
options.compilerArgs += '--enable-preview'
}

tasks.withType(Javadoc).configureEach {
options.addStringOption('Xdoclint:all', '-quiet')
}

test {
useJUnitPlatform()
dependsOn 'cleanTest'
testLogging {
events "skipped", "failed"
exceptionFormat "full"
}
jvmArgs "--enable-preview"
}

javadoc {
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
3 changes: 2 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
29 changes: 17 additions & 12 deletions gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
Expand Down Expand Up @@ -133,26 +131,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
Expand Down Expand Up @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done
fi

# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.

set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
Expand Down
20 changes: 10 additions & 10 deletions gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

Expand All @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2

goto fail

Expand Down
43 changes: 7 additions & 36 deletions src/main/java/com/vonage/client/DynamicEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
Expand Down Expand Up @@ -198,7 +197,7 @@ else if (requestBody instanceof BinaryRequest)

@Override
public final RequestBuilder makeRequest(T requestBody) {
if (requestBody instanceof Jsonable && requestBody.getClass().equals(responseType)) {
if (requestBody instanceof Jsonable && responseType.isAssignableFrom(requestBody.getClass())) {
cachedRequestBody = requestBody;
}
RequestBuilder rqb = createRequestBuilderFromRequestMethod(requestMethod);
Expand Down Expand Up @@ -292,31 +291,11 @@ else if (byte[].class.equals(responseType)) {
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);
}
}
}

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;
return (R) Jsonable.fromJson(deser, (Class<? extends Jsonable>) responseType);
}
else if (Collection.class.isAssignableFrom(responseType)) {
else if (Collection.class.isAssignableFrom(responseType) ||
(responseType.isArray() && Jsonable.class.isAssignableFrom(responseType.getComponentType()))) {
return Jsonable.createDefaultObjectMapper().readValue(deser, responseType);
}
else {
Expand All @@ -335,17 +314,9 @@ private R parseResponseFailure(HttpResponse response) throws IOException, Reflec
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);
}
VonageApiResponseException varex = Jsonable.fromJson(exMessage,
(Class<? extends VonageApiResponseException>) responseExceptionType
);
if (varex.title == null) {
varex.title = response.getStatusLine().getReasonPhrase();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/vonage/client/HttpWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*/
public class HttpWrapper {
private static final String CLIENT_NAME = "vonage-java-sdk";
private static final String CLIENT_VERSION = "8.3.0";
private static final String CLIENT_VERSION = "8.4.0";
private static final String JAVA_VERSION = System.getProperty("java.version");
private static final String USER_AGENT = String.format("%s/%s java/%s", CLIENT_NAME, CLIENT_VERSION, JAVA_VERSION);

Expand Down
21 changes: 11 additions & 10 deletions src/main/java/com/vonage/client/Jsonable.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

/**
* Indicates that a class can be serialized to and parsed from JSON.
Expand All @@ -35,10 +36,9 @@ public interface Jsonable {
* @return A new ObjectMapper with appropriate configuration.
*/
static ObjectMapper createDefaultObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return mapper;
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
}

/**
Expand All @@ -63,14 +63,12 @@ default String toJson() {
* @throws VonageResponseParseException If the JSON was invalid or this class couldn't be updated.
*/
default void updateFromJson(String json) {
if (json == null || json.trim().isEmpty()) {
return;
}
if (json == null || json.trim().isEmpty()) return;
try {
createDefaultObjectMapper().readerForUpdating(this).readValue(json);
}
catch (IOException ex) {
throw new VonageResponseParseException("Failed to produce "+getClass().getSimpleName()+" from json.", ex);
throw new VonageResponseParseException("Failed to produce "+getClass().getSimpleName()+" from JSON.", ex);
}
}

Expand Down Expand Up @@ -108,6 +106,9 @@ static <J extends Jsonable> J fromJson(String json, J... type) {
*/
static <J extends Jsonable> J fromJson(String json, Class<? extends J> jsonable) {
try {
if (Modifier.isAbstract(jsonable.getModifiers())) {
return createDefaultObjectMapper().readValue(json, jsonable);
}
Constructor<? extends J> constructor = jsonable.getDeclaredConstructor();
if (!(constructor.isAccessible())) {
constructor.setAccessible(true);
Expand All @@ -116,8 +117,8 @@ static <J extends Jsonable> J fromJson(String json, Class<? extends J> jsonable)
instance.updateFromJson(json);
return instance;
}
catch (ReflectiveOperationException ex) {
catch (ReflectiveOperationException | JsonProcessingException ex) {
throw new VonageUnexpectedException(ex);
}
}
}
}
Loading

0 comments on commit 5f6687d

Please sign in to comment.