/**
* Copyright 2012 Comcast Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.comcast.cns.util;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.json.JSONWriter;
import com.comcast.cmb.common.util.CMBProperties;
import com.comcast.cns.model.CNSMessage;
import com.comcast.cns.model.CNSMessage.CNSMessageType;
import com.comcast.cns.model.CNSRetryPolicy.CnsBackoffFunction;
import com.comcast.cns.model.CNSSubscription.CnsSubscriptionProtocol;
/**
* Utility functions for cns
* @author aseem, bwolf, jorge
*
*/
public class Util {
public static final int CNS_USER_TOPIC_LIMIT = 100;
// redisPubSubRegex matches redis://[password@]hostname:port/channelname
public static final String redisPubSubRegex = "redis://(([^@]+)@)?([a-z0-9.]+):([0-9]+)/(\\S+)";
public static final Pattern redisPubSubPattern = Pattern.compile(redisPubSubRegex);
public static String generateCnsTopicArn(String topicName, String region, String userId) {
return "arn:cmb:cns:" + region + ":" + userId + ":" + topicName;
}
public static String generateCnsTopicSubscriptionArn(String topicArn, CnsSubscriptionProtocol protocol, String endpoint) throws NoSuchAlgorithmException {
MessageDigest m = MessageDigest.getInstance("MD5");
String name = protocol + ":" + endpoint;
byte bytes[] = m.digest(name.getBytes());
return topicArn + ":" + UUID.nameUUIDFromBytes(bytes).toString();
}
public static String getNameFromTopicArn(String topicArn) {
if (topicArn == null) {
return null;
}
String []elem = topicArn.split(":");
if (elem.length == 6) {
return elem[5];
} else {
return null;
}
}
/**
*
* @param subArn Of the form <topic-arn>:UUID
* @return <topic-arn>
*/
public static String getCnsTopicArn(String subArn) {
String []arr = subArn.split(":");
if (arr.length < 2) {
throw new IllegalArgumentException("Bad format for subscription. Expected <topic-arn>:UUID Got:" + subArn);
}
StringBuffer sb = new StringBuffer(arr[0]);
for (int i = 1; i < arr.length - 1; i++) {
sb.append(":").append(arr[i]);
}
return sb.toString();
}
private static final Pattern topicPattern = Pattern.compile("arn:cmb:cns:[A-Za-z0-9-_]+:[A-Za-z0-9-_]+:[A-Za-z0-9-_]+");
public static boolean isValidTopicArn(String arn) {
if (arn == null) {
return false;
}
Matcher matcher = topicPattern.matcher(arn);
return matcher.matches();
}
public static String getUserIdFromTopicArn(String arn) {
if (!isValidTopicArn(arn)) {
return null;
}
return arn.split(":")[4];
}
private static final Pattern subPattern = Pattern.compile("arn:cmb:cns:[A-Za-z0-9-_]+:[A-Za-z0-9-_]+:[A-Za-z0-9-_]+:[A-Za-z0-9-_]+");
public static boolean isValidSubscriptionArn(String arn) {
Matcher matcher = subPattern.matcher(arn);
return matcher.matches();
}
private static final Pattern topicNamePattern = Pattern.compile("[A-Za-z0-9-_]+");
public static boolean isValidTopicName(String name) {
if (name == null || name.equals("") || name.contains(" ") || name.length() > 256) {
return false;
}
Matcher matcher = topicNamePattern.matcher(name);
return matcher.matches();
}
/**
* Generate the confirmation Json string
* @param arn The top arn for the topic the user is subscribing to.
* @param token the token for confirming the subscription
* @return the Json String
*/
public static String generateConfirmationJson(String topicArn, String token, String messageId) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Writer writer = new PrintWriter(out);
JSONWriter jw = new JSONWriter(writer);
String cnsServiceLocation = CMBProperties.getInstance().getCNSServiceUrl();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); // UTC, no offset
Date now = new Date();
try {
jw = jw.object();
jw.key("Type").value(CNSMessageType.SubscriptionConfirmation);
jw.key("MessageId").value(messageId);
jw.key("Token").value(token);
jw.key("TopicArn").value(topicArn);
jw.key("Message").value("You have chosen to subscribe to the topic "+topicArn+"\\nTo confirm the subscription, visit the SubscribeURL included in this message.");
jw.key("SubscribeURL").value(cnsServiceLocation+"?Action=ConfirmSubscription&TopicArn="+topicArn+"&Token="+token);
jw.key("Timestamp").value(df.format(now));
jw.key("SignatureVersion").value("1");
jw.key("Signature").value("");
jw.key("SigningCertURL").value("");
jw.endObject();
writer.flush();
} catch (Exception e) {
return "";
}
return out.toString();
}
/**
* Generate the Json message to send to all the endpoints except email
* @param arn The topic arn for the topic the user is subscribing to.
* @param message the message to send
* @param subject the subject to send, also included as the subject in email-json
* @return the Json String
*/
public static String generateMessageJson(CNSMessage cnsMessage, CnsSubscriptionProtocol prot) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Writer writer = new PrintWriter(out);
JSONWriter jw = new JSONWriter(writer);
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); //Time is in UTC zone. i,e no offset
String timestamp = df.format(cnsMessage.getTimestamp());
try {
String message = cnsMessage.getProtocolSpecificMessage(prot);
jw = jw.object();
jw.key("Message").value(message);
jw.key("MessageId").value(cnsMessage.getMessageId());
jw.key("Signature").value("");
jw.key("SignatureVersion").value("1");
jw.key("SigningCertURL").value("");
jw.key("Subject").value(cnsMessage.getSubject());
jw.key("Timestamp").value(timestamp);
jw.key("TopicArn").value(cnsMessage.getTopicArn());
jw.key("Type").value(cnsMessage.getMessageType().toString());
String unsubscribeUrl = CMBProperties.getInstance().getCmbUnsubscribeUrl();
if (unsubscribeUrl.contains("%a")) {
unsubscribeUrl = unsubscribeUrl.replace("%a", cnsMessage.getSubscriptionArn());
} else {
unsubscribeUrl += "?Action=Unsubscribe&SubscriptionArn=" + cnsMessage.getSubscriptionArn();
}
jw.key("UnSubscribeURL").value(unsubscribeUrl);
jw.endObject();
writer.flush();
} catch (Exception e) {
return "";
}
return out.toString();
}
public static boolean isPhoneNumber(String phone) {
int size = phone.length();
for (int i=0; i<size; i++) {
Character c = phone.charAt(i);
if ((c == '-') || (c == '+') || (c == '.') || (c == '(') || (c == ')')) {
//skip
} else if((c.compareTo('0') >= 0) && (c.compareTo('9') <= 0)) {
//skip
} else {
return false;
}
}
return true;
}
/**
*
* @param i The number of retry. Must start with 1
* @param maxBackOffRetries The total number of retries allowed
* @param minDelayTarget the minimum retry delay in sec
* @param maxDelayTarget the max retry delay in sec
* @param backOffFunction which backoff function to return
* @return the delay in seconds
*/
public static int getNextRetryDelay(int i, int maxBackOffRetries, int minDelayTarget, int maxDelayTarget, CnsBackoffFunction backOffFunction) {
if (maxBackOffRetries == 0) {
throw new IllegalArgumentException("maxBackOffRetries cannot be 0");
}
double x;
double a;
switch (backOffFunction) {
case linear:
//equation f(i) = slope*(i-1) + minDelayTarget
//calculate slope given f(maxBackOffRetries) = maxDelayTarget = slope(maxBackOffRetries - 1) + minDelayTarget
//=> slope = (maxDelayTarget - minDelayTarget)/ (maxBackOffRetries - 1)
double slope = (double)(maxDelayTarget - minDelayTarget) / (double)(maxBackOffRetries - 1);
return (int) (slope*(i-1) + minDelayTarget);
case geometric:
//figure out x using equation: x^(maxBackOffRetries - 1) + minDelayTarget - 1 = maxDelayTarget
//=> x^(maxBackOffRetries - 1) = maxDelayTarget - minDelayTarget + 1
//=> x = pow(maxDelayTarget - minDelayTarget + 1, 1/(maxBackOffRetries - 1))
// and f(i) = x^(i-1) + minDelayTarget - 1
x = Math.pow(maxDelayTarget - minDelayTarget + 1, 1d/(double)(maxBackOffRetries - 1));
return (int)Math.pow(x, (i-1)) + minDelayTarget - 1;
case exponential:
//equation to use ae^(i-1) + b - a = y. where b = minDelayTarget
//=> ae^(maxBackOffRetries -1) + minDelayTarget - a = maxDelayTarget
//=>a(e^(maxBackOffRetries -1) -1) = maxDelayTarget - minDelayTarget
//=> a = (maxDelayTarget - minDelayTarget) / (e^(maxBackOffRetries -1) -1)
a = (maxDelayTarget - minDelayTarget) / (Math.pow(Math.E, maxBackOffRetries -1) - 1);
return (int) ((a * Math.pow(Math.E, i - 1)) + minDelayTarget - a);
case arithmetic:
//arithmetic is pretty much quadratic for us given euation: ax^2 + b = y
//f(i) = a(i-1)^2 + b
//figure out a using b = minDelayTarget & a(maxBackOffRetries-1)^2 + minDelayTarget = maxDelayTarget
//=> a = (maxDelayTarget-minDelayTarget)/ (maxBackOffRetries-1)^2
a = (maxDelayTarget - minDelayTarget) / Math.pow(maxBackOffRetries - 1, 2);
return (int) (a * Math.pow((i - 1), 2) + minDelayTarget);
default:
throw new IllegalArgumentException("Unknown backoff" + backOffFunction);
}
}
}