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}