001/* 002 * Copyright 2024 Vonage 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.vonage.client.messages; 017 018import com.fasterxml.jackson.annotation.JsonIgnore; 019import com.fasterxml.jackson.annotation.JsonProperty; 020import com.vonage.client.JsonableBaseObject; 021import com.vonage.client.common.E164; 022import com.vonage.client.messages.internal.MessagePayload; 023import java.net.URI; 024import java.util.LinkedHashMap; 025import java.util.Map; 026import java.util.Objects; 027 028/** 029 * Abstract base class of all Messages sent via the Messages v1 API. All subclasses follow a 030 * builder pattern to enable easy construction. The design philosophy is "correct by construction": 031 * that is, validation occurs when calling the {@link Builder#build()} method. It is still the 032 * responsibility of the user to ensure all required parameters are set. The Javadoc for each parameter 033 * in the builders indicates whether it is mandatory (REQUIRED) or not (OPTIONAL). Failure to specify 034 * mandatory parameters will result in {@link NullPointerException} being thrown. Parameters which are 035 * invalid (i.e. have malformed values, such as empty strings) will result in {@link IllegalArgumentException} 036 * being thrown. The documentation on each parameter should provide clarity on parameter restrictions. 037 */ 038public abstract class MessageRequest extends JsonableBaseObject { 039 final MessageType messageType; 040 final Channel channel; 041 protected String from, to; 042 final String clientRef; 043 final URI webhookUrl; 044 final MessagesVersion webhookVersion; 045 protected final Integer ttl; 046 final String text; 047 protected final Map<String, Object> custom; 048 @JsonIgnore protected final MessagePayload media; 049 050 /** 051 * Constructor where all of this class's fields should be set. This is protected 052 * to prevent users form explicitly calling it; this should only be called from the 053 * {@link Builder#build()} method. Subclasses should hide this constructor as well 054 * to avoid potentially confusing users on how to construct this object. 055 * 056 * @param builder The mutable builder object used to assign this MessageRequest's fields from. 057 * @param channel The service to send the message through. 058 * @param messageType The type of message to send. 059 */ 060 protected MessageRequest(Builder<?, ?> builder, Channel channel, MessageType messageType) { 061 this.messageType = Objects.requireNonNull(messageType, "Message type cannot be null"); 062 this.channel = Objects.requireNonNull(channel, "Channel cannot be null"); 063 if (!this.channel.getSupportedOutboundMessageTypes().contains(this.messageType)) { 064 throw new IllegalArgumentException(this.messageType +" cannot be sent via "+ this.channel); 065 } 066 if ((ttl = builder.ttl) != null && ttl < 1) { 067 throw new IllegalArgumentException("TTL must be positive."); 068 } 069 validateSenderAndRecipient(from = builder.from, to = builder.to); 070 clientRef = validateClientReference(builder.clientRef); 071 webhookUrl = builder.webhookUrl; 072 webhookVersion = builder.webhookVersion; 073 074 MessagePayload media = null; 075 Map<String, Object> custom = null; 076 String text = null; 077 078 switch (messageType) { 079 case TEXT: { 080 text = Objects.requireNonNull(builder.text, "Text message cannot be null."); 081 if (text.isEmpty()) { 082 throw new IllegalArgumentException("Text message cannot be blank."); 083 } 084 if (text.length() > maxTextLength()) { 085 throw new IllegalArgumentException( 086 "Text message cannot be longer than " + maxTextLength() + " characters." 087 ); 088 } 089 break; 090 } 091 case CUSTOM: { 092 custom = builder.custom != null ? builder.custom : new LinkedHashMap<>(8); 093 break; 094 } 095 case IMAGE: case AUDIO: case VIDEO: case FILE: case VCARD: { 096 media = new MessagePayload(builder.url, builder.caption, builder.name); 097 break; 098 } 099 default: break; 100 } 101 this.text = text; 102 this.media = media; 103 this.custom = custom; 104 } 105 106 /** 107 * Validates and possibly sanitizes the client reference. 108 * 109 * @param clientRef The clientRef field passed in from the builder. 110 * @return The clientRef to use; usually the same as the argument. 111 */ 112 protected String validateClientReference(String clientRef) { 113 int limit = 100; 114 if (clientRef != null && clientRef.length() > limit) { 115 throw new IllegalArgumentException("Client reference cannot be longer than "+limit+" characters."); 116 } 117 return clientRef; 118 } 119 120 /** 121 * This method is used to validate the format of sender and recipient fields. By default, 122 * this method checks that the recipient is an E164-compliant number and that the sender is not blank. 123 * Subclasses may re-assign the sender and recipient fields to be well-formed / standardised / compliant. 124 * 125 * @param from The sender number or ID passed in from the builder. 126 * @param to The recipient number or ID passed in from the builder. 127 * @throws IllegalArgumentException If the sender or recipient are invalid / malformed. 128 */ 129 protected void validateSenderAndRecipient(String from, String to) throws IllegalArgumentException { 130 if (from == null || from.isEmpty()) { 131 throw new IllegalArgumentException("Sender cannot be empty."); 132 } 133 this.to = new E164(to).toString(); 134 } 135 136 /** 137 * Sets the maximum text length for text messages. 138 * 139 * @return The maximum text message string length. 140 * @since 8.11.0 141 */ 142 @JsonIgnore 143 protected int maxTextLength() { 144 return 1000; 145 } 146 147 @JsonProperty("message_type") 148 public MessageType getMessageType() { 149 return messageType; 150 } 151 152 @JsonProperty("channel") 153 public Channel getChannel() { 154 return channel; 155 } 156 157 @JsonProperty("from") 158 public String getFrom() { 159 return from; 160 } 161 162 @JsonProperty("to") 163 public String getTo() { 164 return to; 165 } 166 167 @JsonProperty("client_ref") 168 public String getClientRef() { 169 return clientRef; 170 } 171 172 @JsonProperty("webhook_url") 173 public URI getWebhookUrl() { 174 return webhookUrl; 175 } 176 177 @JsonProperty("webhook_version") 178 public MessagesVersion getWebhookVersion() { 179 return webhookVersion; 180 } 181 182 @JsonProperty("ttl") 183 protected Integer getTtl() { 184 return ttl; 185 } 186 187 @JsonProperty("text") 188 protected String getText() { 189 return text; 190 } 191 192 @JsonProperty("custom") 193 protected Map<String, ?> getCustom() { 194 return custom; 195 } 196 197 /** 198 * Mutable Builder class, designed to simulate named parameters to allow for convenient 199 * construction of MessageRequests. Subclasses should add their own mutable parameters 200 * and a method for setting them whilst returning the builder, to allow for chaining. 201 * 202 * @param <M> The type of MessageRequest that will be constructed when calling the {@link #build()} method. 203 * @param <B> The type of Builder that will be returned when chaining method calls. This is necessary 204 * to enable the methods to be called in any order and still return the most specific 205 * concrete subtype of builder, rather than this base class. 206 */ 207 @SuppressWarnings("unchecked") 208 public abstract static class Builder<M extends MessageRequest, B extends Builder<? extends M, ? extends B>> { 209 private String from, to, clientRef, text, url, caption, name; 210 private URI webhookUrl; 211 private MessagesVersion webhookVersion; 212 private Integer ttl; 213 private Map<String, Object> custom; 214 215 /** 216 * Protected constructor to prevent users from explicitly creating this object. 217 * This should only be called by the static {@code builder()} method in 218 * the non-abstract subclasses of this builder's parent (declaring) class. 219 */ 220 protected Builder() { 221 } 222 223 /** 224 * (REQUIRED) 225 * Sets the sender number or ID. 226 * 227 * @param from The number or ID to send the message from. 228 * @return This builder. 229 */ 230 public B from(String from) { 231 this.from = from; 232 return (B) this; 233 } 234 235 /** 236 * (REQUIRED) 237 * Custom payload. The schema of a custom object can vary widely according to the channel. 238 * Please consult the relevant documentation for details. 239 * 240 * @param payload The custom payload properties to send as a Map. 241 * @return This builder. 242 */ 243 protected B custom(Map<String, ?> payload) { 244 this.custom = new LinkedHashMap<>(payload); 245 return (B) this; 246 } 247 248 /** 249 * (REQUIRED) 250 * Sets the recipient number or ID. 251 * 252 * @param to The number or ID to send the message to. 253 * @return This builder. 254 */ 255 public B to(String to) { 256 this.to = to; 257 return (B) this; 258 } 259 260 /** 261 * (OPTIONAL) 262 * Sets the client reference, which will be present in every message status. 263 * 264 * @param clientRef Client reference of up to 40 characters. 265 * @return This builder. 266 */ 267 public B clientRef(String clientRef) { 268 this.clientRef = clientRef; 269 return (B) this; 270 } 271 272 /** 273 * (OPTIONAL) 274 * Specifies the URL to which Status Webhook messages will be sent for this particular message. 275 * Overrides account-level and application-level Status Webhook url settings on a per-message basis. 276 * 277 * @param webhookUrl The status webhook URL as a string. 278 * 279 * @return This builder. 280 * 281 * @since 8.1.0 282 */ 283 public B webhookUrl(String webhookUrl) { 284 return webhookUrl(URI.create(webhookUrl)); 285 } 286 287 /** 288 * (OPTIONAL) 289 * Specifies the URL to which Status Webhook messages will be sent for this particular message. 290 * Overrides account-level and application-level Status Webhook url settings on a per-message basis. 291 * 292 * @param webhookUrl The status webhook URL. 293 * 294 * @return This builder. 295 * 296 * @since 8.1.0 297 */ 298 private B webhookUrl(URI webhookUrl) { 299 this.webhookUrl = webhookUrl; 300 return (B) this; 301 } 302 303 /** 304 * Specifies which version of the Messages API will be used to send Status Webhook messages for 305 * this particular message. For example, if {@linkplain MessagesVersion#V0_1} is set, then the 306 * JSON body of Status Webhook messages for this message will be sent in Messages v0.1 format. 307 * Over-rides account-level and application-level API version settings on a per-message basis. 308 * 309 * @param webhookVersion The messages API version enum. 310 * 311 * @return This builder. 312 * 313 * @since 8.1.0 314 */ 315 public B webhookVersion(MessagesVersion webhookVersion) { 316 this.webhookVersion = webhookVersion; 317 return (B) this; 318 } 319 320 /** 321 * (OPTIONAL) 322 * The duration in milliseconds the delivery of a message will be attempted. By default, Vonage attempts 323 * delivery for 72 hours, however the maximum effective value depends on the operator and is typically 324 * 24 to 48 hours. We recommend this value should be kept at its default or at least 30 minutes. 325 * 326 * @param ttl The time-to-live for this message before abandoning delivery attempts, in milliseconds. 327 * 328 * @return This builder. 329 */ 330 protected B ttl(int ttl) { 331 this.ttl = ttl; 332 return (B) this; 333 } 334 335 /** 336 * (REQUIRED) 337 * Sets the text field. 338 * 339 * @param text The text string. 340 * @return This builder. 341 */ 342 protected B text(String text) { 343 this.text = text; 344 return (B) this; 345 } 346 347 /** 348 * (REQUIRED) 349 * Sets the media URL. 350 * 351 * @param url The URL as a string. 352 * @return This builder. 353 */ 354 protected B url(String url) { 355 this.url = url; 356 return (B) this; 357 } 358 359 /** 360 * (OPTIONAL) 361 * Additional text to accompany the media. Must be between 1 and 2000 characters. 362 * 363 * @param caption The caption string. 364 * @return This builder. 365 */ 366 protected B caption(String caption) { 367 this.caption = caption; 368 return (B) this; 369 } 370 371 /** 372 * (OPTIONAL) 373 * The media name. 374 * 375 * @param name The name string. 376 * @return This builder. 377 */ 378 protected B name(String name) { 379 this.name = name; 380 return (B) this; 381 } 382 383 /** 384 * Builds the MessageRequest. 385 * 386 * @return A MessageRequest, populated with all fields from this builder. 387 */ 388 public abstract M build(); 389 } 390}