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}