Build Rest API sending SMS with Amazon SNS and Spring

By | July 30, 2017 | 716 Views

1. Introduction.

This note is to help us to build an API for pushing notifications and SMS to mobile devices using Amazon Simple Notification Service (SNS). Amazon provides the AWS SDK that enables simple and cost effective to send notifications/SMS message to 200+ countries.

2. AWS SNS and Maven configuration.

2.1 AWS SNS Access.

– Sign up for/enable SNS in your AWS Management Console
– Create an IAM user. For example: https://console.aws.amazon.com/iam/home?region=us-east-1.
– Attach the AmazonSNSFullAccess policy to your user as following figure:

– Get Access key which consists of an access key ID (something like AKIAIOSFODNN7EXAMPLE) and a secret access key (something like wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY).

2.2 Maven Configuration.

We need to add AWS SDK SNS and SDK core dependencies to our pom.xml

       <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-sns</artifactId>
            <version>1.11.160</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-core</artifactId>
            <version>1.11.109</version>
        </dependency>

3. Amazon SNS Client.

First of all, we create a SNS client to consume the AWS SNS as a bean in Spring. From SNS dashboard, we create a topic – a communication channel to send messages and subscribe to notifications manually or by SDK.

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.AmazonSNSClientBuilder;
import com.amazonaws.services.sns.model.CreateTopicRequest;
import com.amazonaws.services.sns.model.CreateTopicResult;
import com.amazonaws.services.sns.model.ListTopicsResult;
import com.amazonaws.services.sns.model.Topic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Optional;

@Configuration
public class SNSConfiguration {

    @Value("${aws.sns.accessKey}")
    private String accessKey;
    @Value("${aws.sns.secretKey}")
    private String secretKey;
    @Value("${aws.sns.region}")
    private String region;
    @Value("${aws.sns.topicArn}")
    private String topicArn;
    @Value("${aws.sns.topicName}")
    private String topicName;

    private static final Logger LOGGER = LoggerFactory.getLogger(SNSConfiguration.class);

    @Bean
    public AmazonSNS ssnClient() {
        // Create Amazon SNS Client
        AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
        AmazonSNS snsClient = AmazonSNSClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .withRegion(region)
                .build();

        // OPTIONAL: Check the topic already created or not
        ListTopicsResult listTopicsResult = snsClient.listTopics();
        List<Topic> topics = listTopicsResult.getTopics();

        Optional<Topic> result = topics.stream()
             .filter(t -> topicArn.equalsIgnoreCase(t.getTopicArn())).findAny();

        // Create a new topic if it doesn't exist
        if(!result.isPresent()) {
            createSNSTopic(snsClient);
        }
        return snsClient;
    }

    private CreateTopicResult createSNSTopic(AmazonSNS snsClient) {
        CreateTopicRequest createTopic = new CreateTopicRequest(topicName);
        CreateTopicResult result = snsClient.createTopic(createTopic);
        LOGGER.info("Created topic request: " +
                snsClient.getCachedResponseMetadata(createTopic));
        return  result;
    }
}

Removing the createSNSTopic method if we guarantee the topic already created.

4. SMS Service.

In order to send a SMS message from phone number list, we create a new PublishRequest for each phone number ,and then submit to the SNS topic.

import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.model.*;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.toList;

@Service
public class SMSSenderServiceImpl implement SMSSenderService{

    private static final String MESSAGE_DEFAULT = "Default SMS message.";

    @Value("${aws.sns.SMSType}")
    private String smsType;
    @Value("${aws.sns.phoneNumberRegex}")
    private String phoneNumberRegex;
    @Value("${aws.sns.senderIDRegex}")
    private String senderIDRegex;
    @Value("${aws.sns.senderID}")
    private String senderID;


    private static final Logger LOGGER = LoggerFactory.getLogger(SMSSenderServiceImpl.class);
    private AmazonSNS snsClient;

    @Autowired
    public SMSSenderServiceImpl(AmazonSNS snsClient) {
        this.snsClient = snsClient;
    }


    public boolean sendSMSMessage(final SmsDTO smsDTO) {
        List<String> phoneNumbers = smsDTO.getTo();
       try {
            if(!CollectionUtils.isEmpty(phoneNumbers)) {

                // Filter valid phone numbers and prevent no duplicated. 
                phoneNumbers = phoneNumbers.stream()
                     .filter(p -> isValidPhoneNumber(p)).distinct().collect(toList());

                // Prepare for SNS Client environment like MessageAttributeValue default.
                Map<String, MessageAttributeValue> smsAttributes = new HashMap<>();
                //Map<String, MessageAttributeValue> smsAttributes = setSmsAttributes(smsDTO);
				
				// Send SMS message for each phone numbers.
                phoneNumbers.forEach(p -> sendSMSMessage(snsClient,
                        smsMessageBuilder(smsDTO), p, smsAttributes));
            }
        } catch (Exception e) {
            LOGGER.error("Got error while sending sms {}", e);
            return false;
        }
        return true;
    }

    private String smsMessageBuilder(final SmsDTO smsDTO) {
        final StringBuilder builder = new StringBuilder();
        String message = smsDTO.getMessage();

        if(Strings.isNullOrEmpty(message)) {
            builder.append(MessageFormat.format("{0}. ", MESSAGE_DEFAULT);
        } else {
            builder.append(MessageFormat.format("{0}.",message));
        }
        //TODO Put more message content here

        return builder.toString();
    }

    private void sendSMSMessage(AmazonSNS snsClient, String message,
                               String phoneNumber, Map<String, MessageAttributeValue> smsAttributes) {
        PublishResult result = snsClient.publish(new PublishRequest()
                .withMessage(message)
                .withPhoneNumber(phoneNumber)
                .withMessageAttributes(smsAttributes));
        LOGGER.debug("The message ID {}", result);
    }

   private Map<String, MessageAttributeValue> setSmsAttributes(ShareCampDTO shareCampDTO) {
        Map<String, MessageAttributeValue> smsAttributes = new HashMap<>();

        senderID = Strings.isNullOrEmpty(senderID) ?
                genSenderID(shareCampDTO.getFrom()) : senderID;
        // According to Amazon, SenderID must be 1-11 alpha-numeric characters
       smsAttributes.put("AWS.SNS.SMS.SenderID", new MessageAttributeValue()
                .withStringValue(senderID)
                .withDataType("String"));

        smsAttributes.put("AWS.SNS.SMS.SMSType", new MessageAttributeValue()
                .withStringValue(smsType)
                .withDataType("String"));
        return smsAttributes;
    }

    /**
     *  E.164 is a standard for the phone number structure used for international telecommunication.
     *  Phone numbers that follow this format can have a maximum of 15 digits, and they are prefixed
     *  with the plus character (+) and the country code. For example,
     *  a U.S. phone number in E.164 format would appear as +1XXX5550100.
     * @See at http://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html
     * @see <a href="http://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html">
     *  Sending an SMS Message</a>
     * @param phoneNumber String
     * @return true if valid, otherwise false.
     */
    private boolean isValidPhoneNumber(String phoneNumber) {
        return Pattern.matches(phoneNumberRegex, phoneNumber);
    }

    /**
     * Because Amazon SNS requires that SenderID must be 1-11 alpha-numeric characters.
	 * So we try to generate a correct SenderID.
     * @see  <a href="http://docs.aws.amazon.com/sns/latest/dg/sms_supported-countries.html"></a>
     */
    private String genSenderID(String phoneNumber) {
        String sid = phoneNumber.replaceAll(senderIDRegex, "");
        return sid.length() > 11 ? sid.substring(sid.length() - 11, sid.length()) : sid;
    }
}

5. SMS API.

Using RestController to create the API for sending SMS message like below:

@RestController
public class SMSMessageResource{

    private static final String MESSAGE_SEND_SUCCESS = "Send SMS Message successfully";
    private static final String MESSAGE_SEND_ERROR = "Send SMS Message unsuccessfully";
    private static final String MESSAGE_RES = "message";
	
    @Autowired
    private SMSSenderService smsSenderService;

  
    @RequestMapping(value = "/api/sms", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String, String>> sendSMSMessage(@RequestBody final SmsDTO smsDTO) {
        final Map<String, String> response = new HashMap<>();

        if (smsSenderService.sendSMSMessage(smsDTO)) {
            response.put(MESSAGE_RES, MESSAGE_SEND_SUCCESS);
            return new ResponseEntity<>(response, HttpStatus.OK);
        } else {
            response.put(MESSAGE_RES, MESSAGE_SEND_ERROR);
            return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
        }
    }

}

And SMSDTO looks like:

public class SMSDTO {
    private String from;
    private List<String> to;
    private String message;
    //Generate Getter and Setter
}

6. Run Application.

Run Spring Boot application at port 8080.

POST http://localhost:8080/api/sms/
{
	"from":"+01979423xxx",
	"to": ["+84932903xxx","+44932903xxx"],
	"message": "Send a SMS message by AWS SNS"
}

Check receiver’s phone numbers to see the SMS message. Noticeably, it appears that the SNS service is set to a region that supports SNS messaging. It costs $1.00 to send one million mobile push notifications ($0.50 per million publishes, plus $0.50 per million mobile push notification deliveries).

References

  1. Amazon Simple Notification Service (SNS), https://aws.amazon.com/sns/
  2. AWS ASN Developer Guide. http://docs.aws.amazon.com/sns/latest/dg/welcome.html
  3. Sending SMS from Java with AWS SNS. https://www.linkedin.com/pulse/sending-sms-from-java-aws-sns-steve-perkins

Leave a Reply

Your email address will not be published. Required fields are marked *