001package com.plivo.api.util; 002 003import java.io.UnsupportedEncodingException; 004import java.net.MalformedURLException; 005import java.net.URL; 006import java.nio.charset.StandardCharsets; 007import java.security.InvalidKeyException; 008import java.security.NoSuchAlgorithmException; 009import java.text.Normalizer; 010import java.util.ArrayList; 011import java.util.Arrays; 012import java.util.Base64; 013import java.util.Collections; 014import java.util.HashMap; 015import java.util.LinkedHashMap; 016import java.util.List; 017import java.util.Map; 018import java.util.Map.Entry; 019import java.util.Objects; 020import java.util.Set; 021import java.util.SortedSet; 022import java.util.regex.Pattern; 023import java.util.stream.Collectors; 024import java.util.stream.Stream; 025 026import javax.crypto.Mac; 027import javax.crypto.spec.SecretKeySpec; 028 029import com.fasterxml.jackson.databind.ObjectMapper; 030import com.plivo.api.exceptions.PlivoXmlException; 031 032public class Utils { 033 034 private static final Pattern timePattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,6})?)?$"); 035 036 public static boolean allNotNull(Object... objects) { 037 return Stream.of(objects) 038 .noneMatch(Objects::isNull); 039 } 040 041 public static boolean isSubaccountIdValid(String id) { 042 return id != null && id.startsWith("SA") && id.length() == 20; 043 } 044 045 public static boolean isAccountIdValid(String id) { 046 return id != null && id.startsWith("MA") && id.length() == 20; 047 } 048 049 public static boolean anyNotNull(Object... objects) { 050 return Stream.of(objects) 051 .anyMatch(Objects::nonNull); 052 } 053 054 public static boolean isValidTimeString(String time) { 055 return timePattern.matcher(time).matches(); 056 } 057 058 public static boolean isNonEmptyString(String str) { 059 return !(str == null || str.trim().isEmpty()); 060 } 061 062 063 public static Map<String, Object> objectToMap(ObjectMapper objectMapper, Object object) { 064 Map<String, Object> origMap = objectMapper.convertValue(object, Map.class); 065 Map<String, Object> map = new LinkedHashMap<>(); 066 for (Entry<String, Object> entry : origMap.entrySet()) { 067 if (entry.getValue() != null) { 068 if (entry.getValue() instanceof Map) { 069 Map<String, Object> innerEntries = objectMapper.convertValue(entry.getValue(), Map.class); 070 for (Entry<String, Object> innerEntry : innerEntries.entrySet()) { 071 map.put(entry.getKey() + innerEntry.getKey(), innerEntry.getValue()); 072 } 073 } else { 074 map.put(entry.getKey(), entry.getValue()); 075 } 076 } 077 } 078 return map; 079 } 080 081 private final static String SIGNATURE_ALGORITHM = "HmacSHA256"; 082 083 public static String computeSignature(String url, String nonce, String authToken) 084 throws NoSuchAlgorithmException, InvalidKeyException, MalformedURLException, UnsupportedEncodingException { 085 if (!allNotNull(url, nonce, authToken)) { 086 throw new IllegalArgumentException("url, nonce and authToken must be non-null"); 087 } 088 089 URL parsedURL = new URL(url); 090 String baseUrl = parsedURL.getProtocol() + "://" + parsedURL.getHost(); 091 if (parsedURL.getPort() != -1) { 092 baseUrl += ":" + Integer.toString(parsedURL.getPort()); 093 } 094 baseUrl += parsedURL.getPath(); 095 String payload = baseUrl + nonce; 096 SecretKeySpec signingKey = new SecretKeySpec(authToken.getBytes("UTF-8"), SIGNATURE_ALGORITHM); 097 Mac mac = Mac.getInstance(SIGNATURE_ALGORITHM); 098 mac.init(signingKey); 099 return new String(Base64.getEncoder().encode(mac.doFinal(payload.getBytes("UTF-8")))); 100 } 101 102 public static boolean validateSignature(String url, String nonce, String signature, String authToken) 103 throws NoSuchAlgorithmException, InvalidKeyException, MalformedURLException, UnsupportedEncodingException { 104 return computeSignature(url, nonce, authToken).equals(signature); 105 } 106 107 public static String generateUrl(String url, String method, Map<String, String> params) throws MalformedURLException, UnsupportedEncodingException { 108 String decodedUrl = java.net.URLDecoder.decode(url, StandardCharsets.UTF_8.name()); 109 URL parsedURL = new URL(decodedUrl); 110 String paramString = ""; 111 List<String> keys = new ArrayList<String>(params.keySet()); 112 String uri = parsedURL.getProtocol() + "://" + parsedURL.getHost() + parsedURL.getPath(); 113 int queryParamLength = 0; 114 try { 115 queryParamLength = parsedURL.getQuery().length(); 116 if (params.size() > 0 || queryParamLength > 0) { 117 uri += "?"; 118 } 119 } catch (Exception e) { 120 queryParamLength = 0; 121 uri += "?"; 122 } 123 if (queryParamLength > 0) { 124 if (method == "GET") { 125 Map<String, String> queryParamMap = getMapFromQueryString(parsedURL.getQuery()); 126 for (Entry<String, String> entry : params.entrySet()) { 127 queryParamMap.put(entry.getKey(), entry.getValue()); 128 } 129 uri += GetSortedQueryParamString(queryParamMap, true); 130 } else { 131 uri += GetSortedQueryParamString(getMapFromQueryString(parsedURL.getQuery()), true) + "." + GetSortedQueryParamString(params, false); 132 uri = uri.replace(".$", ""); 133 } 134 } else { 135 if (method == "GET") { 136 uri += GetSortedQueryParamString(params, true); 137 } else { 138 uri += GetSortedQueryParamString(params, false); 139 } 140 } 141 return uri; 142 } 143 144 public static Map<String, String> getMapFromQueryString(String query) { 145 Map<String, String> params = null; 146 if (query.length() == 0) { 147 return params; 148 } 149 params = Arrays.stream(query.split("&")).map(s -> s.split("=", 2)).collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1] : "")); 150 return params; 151 } 152 153 public static String GetSortedQueryParamString(Map<String, String> params, boolean queryParams) { 154 String url = ""; 155 List<String> keys = new ArrayList(params.keySet()); 156 Collections.sort(keys); 157 if (queryParams) { 158 for (String key : keys) { 159 url += key + "=" + params.get(key) + "&"; 160 } 161 url = url.substring(0, url.length() - 1); 162 } else { 163 for (String key : keys) { 164 url += key + params.get(key); 165 } 166 } 167 return url; 168 } 169 170 public static String computeSignatureV3(String url, String nonce, String authToken, String method, Map<String, String> params) 171 throws NoSuchAlgorithmException, InvalidKeyException, MalformedURLException, UnsupportedEncodingException { 172 if (!allNotNull(url, nonce, authToken)) { 173 throw new IllegalArgumentException("url, nonce and authToken must be non-null"); 174 } 175 176 URL parsedURL = new URL(url); 177 String payload = generateUrl(url, method, params) + "." + nonce; 178 SecretKeySpec signingKey = new SecretKeySpec(authToken.getBytes("UTF-8"), SIGNATURE_ALGORITHM); 179 Mac mac = Mac.getInstance(SIGNATURE_ALGORITHM); 180 mac.init(signingKey); 181 return new String(Base64.getEncoder().encode(mac.doFinal(payload.getBytes("UTF-8")))); 182 } 183 184 public static boolean validateSignatureV3(String url, String nonce, String signature, String authToken, String method, Map<String, String>... params) 185 throws NoSuchAlgorithmException, InvalidKeyException, MalformedURLException, UnsupportedEncodingException { 186 Map<String, String> parameters = new HashMap<String, String>(); 187 if(params.length > 0){ 188 parameters = params[0]; 189 } 190 List<String> splitSignature = Arrays.asList(signature.split(",")); 191 return splitSignature.contains(computeSignatureV3(url, nonce, authToken, method, parameters)); 192 } 193 194 private static Map<String, List<String>> getLanguageVoices() { 195 Map<String, List<String>> languageVoices = new HashMap<>(); 196 languageVoices.put("arb", new ArrayList<String>(Arrays.asList("Zeina"))); 197 languageVoices.put("cmn-CN", new ArrayList<String>(Arrays.asList("Zhiyu"))); 198 languageVoices.put("da-DK", new ArrayList<String>(Arrays.asList("Naja", "Mads"))); 199 languageVoices.put("nl-NL", new ArrayList<String>(Arrays.asList("Lotte", "Ruben"))); 200 languageVoices.put("en-AU", new ArrayList<String>(Arrays.asList("Nicole", "Russell"))); 201 languageVoices.put("en-GB", new ArrayList<String>(Arrays.asList("Amy", "Emma", "Brian"))); 202 languageVoices.put("en-IN", new ArrayList<String>(Arrays.asList("Raveena", "Aditi"))); 203 languageVoices.put("en-US", new ArrayList<String>(Arrays.asList("Joanna", "Salli", "Kendra", "Kimberly", "Ivy", "Matthew", "Justin", "Joey"))); 204 languageVoices.put("en-GB-WLS", new ArrayList<String>(Arrays.asList("Geraint"))); 205 languageVoices.put("fr-CA", new ArrayList<String>(Arrays.asList("Chantal", "Chantal"))); 206 languageVoices.put("fr-FR", new ArrayList<String>(Arrays.asList("Léa", "Céline", "Mathieu"))); 207 languageVoices.put("de-DE", new ArrayList<String>(Arrays.asList("Vicki", "Hans"))); 208 languageVoices.put("hi-IN", new ArrayList<String>(Arrays.asList("Aditi"))); 209 languageVoices.put("is-IS", new ArrayList<String>(Arrays.asList("Dóra", "Karl"))); 210 languageVoices.put("it-IT", new ArrayList<String>(Arrays.asList("Carla", "Giorgio"))); 211 languageVoices.put("ja-JP", new ArrayList<String>(Arrays.asList("Mizuki", "Takumi"))); 212 languageVoices.put("ko-KR", new ArrayList<String>(Arrays.asList("Seoyeon"))); 213 languageVoices.put("nb-NO", new ArrayList<String>(Arrays.asList("Liv"))); 214 languageVoices.put("pl-PL", new ArrayList<String>(Arrays.asList("Ewa", "Maja", "Jacek", "Jan"))); 215 languageVoices.put("pt-BR", new ArrayList<String>(Arrays.asList("Vitória", "Ricardo"))); 216 languageVoices.put("pt-PT", new ArrayList<String>(Arrays.asList("Inês", "Cristiano"))); 217 languageVoices.put("ro-RO", new ArrayList<String>(Arrays.asList("Carmen"))); 218 languageVoices.put("ru-RU", new ArrayList<String>(Arrays.asList("Tatyana", "Maxim"))); 219 languageVoices.put("es-ES", new ArrayList<String>(Arrays.asList("Conchita", "Lucia", "Enrique"))); 220 languageVoices.put("es-MX", new ArrayList<String>(Arrays.asList("Mia"))); 221 languageVoices.put("es-US", new ArrayList<String>(Arrays.asList("Penélope", "Miguel"))); 222 languageVoices.put("sv-SE", new ArrayList<String>(Arrays.asList("Astrid"))); 223 languageVoices.put("tr-TR", new ArrayList<String>(Arrays.asList("Filiz"))); 224 languageVoices.put("cy-GB", new ArrayList<String>(Arrays.asList("Gwyneth"))); 225 226 return languageVoices; 227 } 228 229 public static void validateLanguageVoice(String language, String voice) throws PlivoXmlException { 230 String[] voiceParts = voice.split("\\."); 231 System.out.println(language); 232 if (voiceParts.length != 2 || !voiceParts[0].equals("Polly")) { 233 throw new PlivoXmlException("XML Validation Error: Invalid language. Voice " + voice + " is not valid. " + 234 "Refer <https://www.plivo.com/docs/voice/getting-started/advanced/getting-started-with-ssml/#ssml-voices> for the list of supported voices."); 235 } 236 237 Map<String, List<String>> languageVoices = getLanguageVoices(); 238 // Validate supported languages. 239 if (languageVoices.get(language) == null || languageVoices.get(language).isEmpty()) { 240 throw new PlivoXmlException("XML Validation Error: Invalid language. Language " + language + " is not supported."); 241 } 242 243 // Transform the available language voices and the voice name into a common format. 244 List<String> availableLanguageVoices = languageVoices.get(language); 245 for (int i = 0; i < availableLanguageVoices.size(); i++) { 246 availableLanguageVoices.set(i, transformString(availableLanguageVoices.get(i))); 247 } 248 String transformedVoiceName = transformString(voiceParts[1]); 249 250 if (!voiceParts[1].equals("*") && !availableLanguageVoices.contains(transformedVoiceName)) { 251 throw new PlivoXmlException("XML Validation Error: <Speak> voice '" + voice + 252 "' is not valid. Refer <https://www.plivo.com/docs/voice/getting-started/advanced/getting-started-with-ssml/#ssml-voices> for list of supported voices."); 253 } 254 } 255 256 public static String transformString(String s) { 257 String transformedString; 258 // Replace all accented characters with comparable english alphabets 259 transformedString = Normalizer.normalize(s.trim(), Normalizer.Form.NFD); 260 Pattern pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); 261 transformedString = pattern.matcher(transformedString).replaceAll(""); 262 263 // To title case and replace spaces with '_' 264 transformedString = (new ArrayList<>(Arrays.asList(transformedString.toLowerCase().split(" ")))) 265 .stream() 266 .map(word -> Character.toTitleCase(word.charAt(0)) + word.substring(1)) 267 .collect(Collectors.joining("_")); 268 return transformedString; 269 } 270}