WebSocketConfig
inside com.twilio.websocketcallback
package with the following contents:package com.example.websocketcallback; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket").withSockJS(); } }
@EnableWebSocketMessageBroker
is used to enable our WebSocket server. We implement WebSocketMessageBrokerConfigurer
interface and provide an implementation for some of its methods to configure the WebSocket connection.configureMessageBroker
, we’re configuring a message broker that will be used to route messages from one client to another./topic
should be routed to the message broker. Message broker broadcasts messages to all the connected clients who are subscribed to a particular topic. Below is the pictorial representation of the WebSockets and the implementation details. /app
. After processing the message, the controller will send it to the broker channel.registerStompEndpoints
, we register a WebSocket endpoint, that the clients use to connect to our WebSocket server.withSockJS()
with the endpoint configuration. SockJS is used to enable fallback options for browsers that don’t support WebSocket.build.gradle
file with the associated Twilio and WebSockets dependency libraries. dependencies { implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation group: "com.twilio.sdk", name: "twilio", version : "7.47.2" testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } }
to
and message
properties and the corresponding methods.package com.twilio.websocketcallback.domain; public class SMS { private String to; private String message; public String getTo() { return to; } public void setTo(String to) { this.to = to; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } @Override public String toString() { return "SMS{" + "to='" + to + '\'' + ", message='" + message + '\'' + '}'; } }
@Controller
classes. For example, the SMSController
is mapped to handle messages to destination /sms
.package com.example.websocketcallback; import com.twilio.exception.ApiException; import com.twilio.websocketcallback.domain.SMS; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @RestController public class SMSController { @Autowired SMSService service; @Autowired private SimpMessagingTemplate webSocket; private final String TOPIC_DESTINATION = "/topic/sms"; @RequestMapping(value = "/sms", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public void smsSubmit(@RequestBody SMS sms) { try{ service.send(sms); } catch(ApiException e){ webSocket.convertAndSend(TOPIC_DESTINATION, getTimeStamp() + ": Error sending the SMS: "+e.getMessage()); throw e; } webSocket.convertAndSend(TOPIC_DESTINATION, getTimeStamp() + ": SMS has been sent!: "+sms.getTo()); } @RequestMapping(value = "/smscallback", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public void smsCallback(@RequestBody MultiValueMap<String, String> map) { service.receive(map); webSocket.convertAndSend(TOPIC_DESTINATION, getTimeStamp() + ": Twilio has made a callback request! Here are the contents: "+map.toString()); } private String getTimeStamp() { return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()); } }
@RequestMapping
annotation ensures that if a message is sent to the destination /sms
through POST method. The content_type accepted by the consumer is set in the line consumes = MediaType.APPLICATION_JSON_VALUE.
produces = MediaType.APPLICATION_JSON_VALUE
is the value that can be produced by the mapped handler. produces
consists of one or more media types, one of which must be chosen via content negotiation against the "acceptable" media types of the request. Typically those are extracted from the "Accept"
header but may be derived from query parameters, or somewhere else. @RequestBody
SMS
object, which is passed into SMSSubmit()
./topic/sms
as specified in the webSocket.convertAndSend()
method. This method is used to send messages to connected clients from any part of the application. Any application component can send messages to the"brokerChannel" and is done by SimpMessagingTemplate
injection to send messages.message
and to
fields are received from the UI, these values are parsed and sent to the SMS service called send
method. This will send outgoing SMS messages from your Twilio phone number to the text-capable phone number you input in the UI. Twilio sends SMS on-behalf of the actual wired phone number. With the programmable SMS, we can build logic to send SMS with a message body and the destination phone number and allow Twilio to do its magic.smsCallback
is a callback handler method that receives the callback from the Twilio endpoint. It consumes a request content-type
header of type MediaType.APPLICATION_FORM_URLENCODED_VALUE
and produces a negotiable mediatype
of type MediaType.APPLICATION_JSON_VALUE
. This mediatype is then processed by our frontend js engine. Twilio sends a callback in URL_ENCODED form values, hence they need to be formatted into a type that is easily consumable by your application. ./bash_profile
(environment) file on your development machine, or another suitable place depending on your operating system. Spring can extract data through externalized configuration. package com.example. websocketcallback; import com.twilio. websocketcallback.domain.SMS; import com.twilio.rest.api.v2010.account.Message; import com.twilio.type.PhoneNumber; import com.twilio.Twilio; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; import java.net.URI; @Component public class SMSService { @Value("#{systemEnvironment['TWILIO_ACCOUNT_SID']}") private String ACCOUNT_SID; @Value("#{systemEnvironment['TWILIO_AUTH_TOKEN']}") private String AUTH_TOKEN; @Value("#{systemEnvironment['TWILIO_PHONE_NUMBER']}") private String FROM_NUMBER; public void send(SMS sms) { Twilio.init(ACCOUNT_SID, AUTH_TOKEN); Message message = Message.creator(new PhoneNumber(sms.getTo()), new PhoneNumber(FROM_NUMBER), sms.getMessage()) .create(); System.out.println("here is my id:"+message.getSid());// Unique resource ID created to manage this transaction } public void receive(MultiValueMap<String, String> smscallback) { } }
src/main/resources/static.
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Hello WebSocket</title> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet"> <link href="/main.css" rel="stylesheet"> <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script> <script src="/app.js"></script> </head> <body> <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2></noscript> <div id="main-content" class="container"> <div class="col-md-6"> <form class="form-inline" id ="smsForm" method="post"> <div class="form-group"> <p>To Number: <input type="text" id="to" /></p> <br/><p>Message: <textarea id="message" > </textarea></p> </div> <p> <button id="send" class="btn btn-primary" type="submit">Send</button></p> </form> </div> </div> <div class="row"> <div class="col-md-12"> <table id="conversation" class="table table-striped"> <thead> <tr> <th>Message Status dashboard</th> </tr> </thead> <tbody id="messages"> </tbody> </table> </div> </div> </div> </body> </html>
SockJS
and STOMP
javascript libraries that will be used to communicate with our server using STOMP over WebSocket. We’re also importing here an app.js
file which contains the logic of our client application.var stompClient = null; function setConnected(connected) { if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#messages").html(""); } function connect() { var socket = new SockJS('/gs-guide-websocket'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/sms', function (sms) { showData(sms.body); }); }); } function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } setConnected(false); console.log("Disconnected"); } function showData(message) { $("#messages").append("<tr><td>" + message + "</td></tr>"); } function processForm(e) { if (e.preventDefault) e.preventDefault(); var form = document.getElementById('smsForm'); var data = {}; for (var i = 0, ii = form.length; i < ii -1; ++i) { var input = form[i]; if (input.id) { data[input.id] = input.value; } } // construct an HTTP request var xhr = new XMLHttpRequest(); xhr.open("post", "/sms", true); xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); // send the collected data as JSON xhr.send(JSON.stringify(data)); return false; } $(function () { let form = document.getElementById('smsForm'); if (form.attachEvent) { form.attachEvent("submit", processForm); } else { form.addEventListener("submit", processForm); } // collect the form data while iterating over the inputs }); connect();
./gradlew bootRun
.