Skip to content

Commit 65fa020

Browse files
authored
feat: Native failover in Messages API (#586)
1 parent 23e8d94 commit 65fa020

File tree

11 files changed

+149
-18
lines changed

11 files changed

+149
-18
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
# [9.3.0] - 2025-05-07
6+
- Added support for native failover in Messages API
7+
58
# [9.2.0] - 2025-04-30
69
- Added support for setting additional request headers in `HttpConfig`
710
- Allow setting `HttpClient` and `HttpConfig` on `HttpWrapper`

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Add the following to your `build.gradle` or `build.gradle.kts` file:
7474

7575
```groovy
7676
dependencies {
77-
implementation("com.vonage:server-sdk:9.2.0")
77+
implementation("com.vonage:server-sdk:9.3.0")
7878
}
7979
```
8080

@@ -85,7 +85,7 @@ Add the following to the `<dependencies>` section of your `pom.xml` file:
8585
<dependency>
8686
<groupId>com.vonage</groupId>
8787
<artifactId>server-sdk</artifactId>
88-
<version>9.2.0</version>
88+
<version>9.3.0</version>
8989
</dependency>
9090
```
9191

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.vonage</groupId>
77
<artifactId>server-sdk</artifactId>
8-
<version>9.2.0</version>
8+
<version>9.3.0</version>
99

1010
<name>Vonage Java Server SDK</name>
1111
<description>Java client for Vonage APIs</description>

src/main/java/com/vonage/client/HttpWrapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
public class HttpWrapper {
3838
private static final String
3939
CLIENT_NAME = "vonage-java-sdk",
40-
CLIENT_VERSION = "9.2.0",
40+
CLIENT_VERSION = "9.3.0",
4141
JAVA_VERSION = System.getProperty("java.version"),
4242
USER_AGENT = String.format("%s/%s java/%s", CLIENT_NAME, CLIENT_VERSION, JAVA_VERSION);
4343

src/main/java/com/vonage/client/messages/MessageRequest.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
import com.vonage.client.common.MessageType;
2323
import com.vonage.client.messages.internal.MessagePayload;
2424
import java.net.URI;
25-
import java.util.LinkedHashMap;
26-
import java.util.Map;
27-
import java.util.Objects;
25+
import java.util.*;
2826

2927
/**
3028
* Abstract base class of all Messages sent via the Messages v1 API. All subclasses follow a
@@ -43,6 +41,7 @@ public abstract class MessageRequest extends JsonableBaseObject {
4341
final String clientRef;
4442
final URI webhookUrl;
4543
final MessagesVersion webhookVersion;
44+
final List<MessageRequest> failover;
4645
protected final Integer ttl;
4746
final String text;
4847
protected final Map<String, Object> custom;
@@ -71,6 +70,7 @@ protected MessageRequest(Builder<?, ?> builder, Channel channel, MessageType mes
7170
clientRef = validateClientReference(builder.clientRef);
7271
webhookUrl = builder.webhookUrl;
7372
webhookVersion = builder.webhookVersion;
73+
failover = builder.failover;
7474

7575
MessagePayload media = null;
7676
Map<String, Object> custom = null;
@@ -180,6 +180,11 @@ public MessagesVersion getWebhookVersion() {
180180
return webhookVersion;
181181
}
182182

183+
@JsonProperty("failover")
184+
public List<MessageRequest> getFailover() {
185+
return failover;
186+
}
187+
183188
@JsonProperty("ttl")
184189
protected Integer getTtl() {
185190
return ttl;
@@ -210,6 +215,7 @@ public abstract static class Builder<M extends MessageRequest, B extends Builder
210215
private String from, to, clientRef, text, url, caption, name;
211216
private URI webhookUrl;
212217
private MessagesVersion webhookVersion;
218+
private List<MessageRequest> failover;
213219
private Integer ttl;
214220
private Map<String, Object> custom;
215221

@@ -381,6 +387,33 @@ protected B name(String name) {
381387
return (B) this;
382388
}
383389

390+
/**
391+
* (OPTIONAL)
392+
* Sets the failover messages to be sent if the primary message fails.
393+
*
394+
* @param failover The failover message(s).
395+
* @return This builder.
396+
*
397+
* @since 9.3.0
398+
*/
399+
public B failover(MessageRequest... failover) {
400+
return failover(Arrays.asList(failover));
401+
}
402+
403+
/**
404+
* (OPTIONAL)
405+
* Sets the failover messages to be sent if the primary message fails.
406+
*
407+
* @param failover The list of failover messages.
408+
* @return This builder.
409+
*
410+
* @since 9.3.0
411+
*/
412+
public B failover(List<MessageRequest> failover) {
413+
this.failover = failover;
414+
return (B) this;
415+
}
416+
384417
/**
385418
* Builds the MessageRequest.
386419
*

src/main/java/com/vonage/client/messages/MessageResponse.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
* the returned response (HTTP 202 payload) is always the same format.
2525
*/
2626
public class MessageResponse extends JsonableBaseObject {
27-
protected UUID messageUuid;
27+
private UUID messageUuid;
28+
private String workflowId;
2829

2930
/**
3031
* Protected to prevent users from explicitly creating this object.
@@ -41,4 +42,16 @@ protected MessageResponse() {
4142
public UUID getMessageUuid() {
4243
return messageUuid;
4344
}
45+
46+
/**
47+
* Returns the workflow ID of the message that was sent to track the failover (if applicable).
48+
*
49+
* @return The failover workflow ID, or {@code null} if absent / not applicable.
50+
*
51+
* @since 9.3.0
52+
*/
53+
@JsonProperty("workflow_id")
54+
public String getWorkflowId() {
55+
return workflowId;
56+
}
4457
}

src/main/java/com/vonage/client/messages/MessageStatus.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,44 @@ public String toString() {
5858
}
5959
}
6060

61+
/**
62+
* Represents the {@code workflow} object in the status webhook for native failover.
63+
*
64+
* @since 9.3.0
65+
*/
66+
public static final class Workflow extends JsonableBaseObject {
67+
@JsonProperty("id") String id;
68+
@JsonProperty("item_number") Integer itemNumber;
69+
@JsonProperty("items_total") @JsonAlias("total_items") Integer totalItems;
70+
71+
/**
72+
* ID of the workflow.
73+
*
74+
* @return The workflow type as a string.
75+
*/
76+
public String getId() {
77+
return id;
78+
}
79+
80+
/**
81+
* The message number (sequence) in the workflow.
82+
*
83+
* @return The index of this message in the workflow as integer.
84+
*/
85+
public Integer getItemNumber() {
86+
return itemNumber;
87+
}
88+
89+
/**
90+
* Total number of messages in this workflow.
91+
*
92+
* @return The number of messages (including failover) in the workflow as integer.
93+
*/
94+
public Integer getTotalItems() {
95+
return totalItems;
96+
}
97+
}
98+
6199
/**
62100
* Describes the error that was encountered when sending the message.
63101
*/
@@ -174,6 +212,7 @@ protected MessageStatus() {
174212
@JsonProperty("channel") protected Channel channel;
175213
@JsonProperty("client_ref") protected String clientRef;
176214
@JsonProperty("error") protected Error error;
215+
@JsonProperty("workflow") protected Workflow workflow;
177216
@JsonProperty("usage") protected Usage usage;
178217

179218
@JsonProperty("destination") private Destination destination;
@@ -254,6 +293,17 @@ public Error getError() {
254293
return error;
255294
}
256295

296+
/**
297+
* If the message was sent with a failover workflow, the details of this will be returned here.
298+
*
299+
* @return The workflow object, or {@code null} if absent / not applicable.
300+
*
301+
* @since 9.3.0
302+
*/
303+
public Workflow getWorkflow() {
304+
return workflow;
305+
}
306+
257307
/**
258308
* Describes the cost of the message that was sent.
259309
*

src/test/java/com/vonage/client/TestUtils.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,11 @@ public static HttpWrapper httpWrapperWithAllAuthMethods() {
159159
}
160160

161161

162-
// TODO make package-private after removing Meetings
163-
public static CloseableHttpClient stubHttpClient(int statusCode) throws Exception {
162+
static CloseableHttpClient stubHttpClient(int statusCode) throws Exception {
164163
return stubHttpClient(statusCode, "");
165164
}
166165

167-
// TODO make package-private after removing Meetings
168-
public static CloseableHttpClient stubHttpClient(int statusCode, String content, String... additionalReturns) throws Exception {
166+
static CloseableHttpClient stubHttpClient(int statusCode, String content, String... additionalReturns) throws Exception {
169167
CloseableHttpClient result = mock(CloseableHttpClient.class);
170168

171169
CloseableHttpResponse response = mock(CloseableHttpResponse.class);

src/test/java/com/vonage/client/messages/MessageRequestTest.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,21 @@ private ConcreteMessageRequest(Builder builder) {
4848
}
4949

5050
@Test
51-
public void testSerializeAllFields() {
51+
public void testSerializeAllFieldsWithFailover() {
5252
MessageRequest smr = ConcreteMessageRequest.builder(MessageType.VIDEO, Channel.MMS)
5353
.from("447900000009").to("12002009000")
5454
.url("https://example.com/video.mp4")
5555
.clientRef("<40 character string")
5656
.webhookUrl("https://example.com/status")
57-
.webhookVersion(MessagesVersion.V1).build();
57+
.webhookVersion(MessagesVersion.V1)
58+
.failover(ConcreteMessageRequest.builder(MessageType.IMAGE, Channel.RCS)
59+
.from("12002009001").to("44790000002")
60+
.url("https://example.org/image.jpg").build(),
61+
ConcreteMessageRequest.builder(MessageType.TEXT, Channel.SMS)
62+
.from("SenderID").to("44790000004")
63+
.text("Fallback text message").build()
64+
)
65+
.build();
5866

5967
String generatedJson = smr.toJson();
6068
assertTrue(generatedJson.contains("\"client_ref\":\"<40 character string\""));
@@ -64,6 +72,11 @@ public void testSerializeAllFields() {
6472
assertTrue(generatedJson.contains("\"to\":\"12002009000\""));
6573
assertTrue(generatedJson.contains("\"channel\":\"mms\""));
6674
assertTrue(generatedJson.contains("\"message_type\":\"video\""));
75+
assertTrue(generatedJson.contains("\"failover\":[" +
76+
"{\"message_type\":\"image\",\"channel\":\"rcs\",\"from\":\"12002009001\",\"to\":\"44790000002\"}," +
77+
"{\"message_type\":\"text\",\"channel\":\"sms\",\"from\":\"SenderID\",\"to\":\"44790000004\"," +
78+
"\"text\":\"Fallback text message\"}]"
79+
));
6780
assertFalse(generatedJson.contains("https://example.com/video.mp4"));
6881
}
6982

@@ -200,9 +213,7 @@ public void testConstructNoChannel() {
200213
@Test
201214
public void testConstructLongClientRef() {
202215
StringBuilder clientRef = new StringBuilder(41);
203-
for (int i = 0; i < 99; i++) {
204-
clientRef.append('c');
205-
}
216+
clientRef.append("c".repeat(99));
206217

207218
ConcreteMessageRequest.Builder builder = ConcreteMessageRequest
208219
.builder(MessageType.FILE, Channel.RCS)

src/test/java/com/vonage/client/messages/MessageResponseTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ public class MessageResponseTest {
2727
@Test
2828
public void testConstructFromValidJson() {
2929
UUID uuid = UUID.randomUUID();
30-
MessageResponse response = Jsonable.fromJson("{\"message_uuid\":\""+uuid+"\"}");
30+
String workflowId = "3TcNjguphQTKshBPRJUVYx6sREpqvhgghJza6sWMk9ztEQQdiW9vwGG";
31+
MessageResponse response = Jsonable.fromJson("{\"message_uuid\":\""+uuid+"\",\"workflow_id\":\""+workflowId+"\"}");
3132
assertEquals(uuid, response.getMessageUuid());
33+
assertEquals(workflowId, response.getWorkflowId());
3234
String toString = response.toString();
3335
assertTrue(toString.contains("MessageResponse"));
3436
assertTrue(toString.contains(uuid.toString()));
@@ -39,6 +41,7 @@ public void testConstructFromValidJson() {
3941
public void testConstructFromEmptyJson() {
4042
MessageResponse response = Jsonable.fromJson("{}");
4143
assertNull(response.getMessageUuid());
44+
assertNull(response.getWorkflowId());
4245
TestUtils.testJsonableBaseObject(response);
4346
}
4447

src/test/java/com/vonage/client/messages/MessageStatusTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public void testSerdesAllFields() {
4848
error.title = String.valueOf(title);
4949
error.detail = detail;
5050
error.instance = instance;
51+
MessageStatus.Workflow workflow = new MessageStatus.Workflow();
52+
workflow.id = "1001";
53+
workflow.itemNumber = 2;
54+
workflow.totalItems = 3;
55+
5156
MessageStatus.Usage usage = new MessageStatus.Usage();
5257
usage.price = price;
5358
usage.currency = currency;
@@ -78,6 +83,11 @@ public void testSerdesAllFields() {
7883
" \"detail\": \""+detail+"\",\n" +
7984
" \"instance\": \""+instance+"\"\n" +
8085
" },\n" +
86+
" \"workflow\": {\n" +
87+
" \"id\": \"1001\",\n" +
88+
" \"item_number\": \"2\",\n" +
89+
" \"total_items\": \"3\"\n" +
90+
" },\n" +
8191
" \"usage\": {\n" +
8292
" \"currency\": \""+currency+"\",\n" +
8393
" \"price\": \""+price+"\"\n" +
@@ -111,6 +121,8 @@ public void testSerdesAllFields() {
111121
assertEquals("sms", channel.toString());
112122
assertEquals(error, ms.getError());
113123
assertEquals(error.toString(), ms.getError().toString());
124+
assertEquals(workflow, ms.getWorkflow());
125+
assertEquals(workflow.toString(), ms.getWorkflow().toString());
114126
assertEquals(usage, ms.getUsage());
115127
assertEquals(usage.toString(), ms.getUsage().toString());
116128
assertEquals(networkCode, ms.getDestinationNetworkCode());
@@ -148,6 +160,7 @@ public void testSerdesRequiredFields() {
148160
assertEquals("undeliverable", status.toString());
149161
assertEquals(channel, ms.getChannel());
150162
assertEquals("mms", channel.toString());
163+
assertNull(ms.getWorkflow());
151164
assertNull(ms.getSmsTotalCount());
152165
assertNull(ms.getDestinationNetworkCode());
153166
assertNull(ms.getWhatsappConversationType());
@@ -173,6 +186,7 @@ public void testDeserializeUnknownProperties() {
173186
" \"currency\": \"EUR\",\n" +
174187
" \"price\": \"0.0333\"\n" +
175188
" },\n" +
189+
" \"workflow\": {},\n" +
176190
" \"client_ref\": \"string\",\n" +
177191
" \"channel\": \"whatsapp\",\n" +
178192
" \"wubwub\": {\n" +
@@ -187,6 +201,12 @@ public void testDeserializeUnknownProperties() {
187201
MessageStatus ms = MessageStatus.fromJson(json);
188202
testJsonableBaseObject(ms);
189203

204+
MessageStatus.Workflow workflow = ms.getWorkflow();
205+
assertNotNull(workflow);
206+
assertNull(workflow.getId());
207+
assertNull(workflow.getItemNumber());
208+
assertNull(workflow.getTotalItems());
209+
190210
Map<String, ?> unknown = ms.getAdditionalProperties();
191211
assertNotNull(unknown);
192212
assertEquals(1, unknown.size());

0 commit comments

Comments
 (0)