<commit_before>/*******************************************************************************
 * Copyright (c) 2016, 2017 Stichting Yona Foundation This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *******************************************************************************/
package nu.yona.server.sms;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import nu.yona.server.exceptions.SmsException;
import nu.yona.server.properties.SmsProperties;
import nu.yona.server.properties.YonaProperties;
import nu.yona.server.util.ThymeleafUtil;

@Service
public class PlivoSmsService implements SmsService
{
	private static final Logger logger = LoggerFactory.getLogger(PlivoSmsService.class);

	@Autowired
	private YonaProperties yonaProperties;

	@Autowired
	@Qualifier("smsTemplateEngine")
	private TemplateEngine templateEngine;

	@Override
	public void send(String phoneNumber, SmsTemplate messageTemplate, Map<String, Object> templateParameters)
	{
		Context ctx = ThymeleafUtil.createContext();
		templateParameters.entrySet().stream().forEach(e -> ctx.setVariable(e.getKey(), e.getValue()));

		String message = templateEngine.process(messageTemplate.getName() + ".txt", ctx);
		logger.info("Sending SMS to number '{}'. Message: {}", phoneNumber, message);

		if (!yonaProperties.getSms().isEnabled())
		{
			logger.info("SMS sending is disabled. No message has been sent.");
			return;
		}

		try (CloseableHttpClient httpClient = HttpClientBuilder.create().build())
		{
			String requestMessageStr = createRequestJson(phoneNumber, message);
			HttpPost httpRequest = createHttpRequest(requestMessageStr);
			HttpClientContext httpClientContext = createHttpClientContext();
			HttpResponse httpResponse = httpClient.execute(httpRequest, httpClientContext);
			HttpEntity entity = httpResponse.getEntity();

			int httpResponseCode = httpResponse.getStatusLine().getStatusCode();
			String httpResponseBody = EntityUtils.toString(entity);
			if (httpResponseCode != HttpStatus.SC_ACCEPTED)
			{
				throw SmsException.smsSendingFailed(httpResponseCode, httpResponseBody);
			}
		}
		catch (IOException e)
		{
			throw SmsException.smsSendingFailed(e);
		}

		logger.info("SMS sent succesfully.");
	}

	private HttpClientContext createHttpClientContext()
	{
		try
		{
			SmsProperties smsProperties = yonaProperties.getSms();

			URI uri = getPlivoUrl(smsProperties);
			CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
			credentialsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
					new UsernamePasswordCredentials(smsProperties.getPlivoAuthId(), smsProperties.getPlivoAuthToken()));

			HttpClientContext httpClientContext = HttpClientContext.create();
			httpClientContext.setCredentialsProvider(credentialsProvider);
			return httpClientContext;
		}
		catch (URISyntaxException e)
		{
			throw SmsException.smsSendingFailed(e);
		}
	}

	private URI getPlivoUrl(SmsProperties smsProperties) throws URISyntaxException
	{
		return new URI(MessageFormat.format(smsProperties.getPlivoUrl(), smsProperties.getPlivoAuthId()));
	}

	private String createRequestJson(String phoneNumber, String message)
	{
		try
		{
			Map<String, Object> requestMessage = new HashMap<>();
			requestMessage.put("src", determineSender(phoneNumber));
			requestMessage.put("dst", phoneNumber.replace("+", ""));
			requestMessage.put("text", message);

			return new ObjectMapper().writeValueAsString(requestMessage);
		}
		catch (JsonProcessingException e)
		{
			throw SmsException.smsSendingFailed(e);
		}
	}

	String determineSender(String targetPhoneNumber)
	{
		for (String prefix : yonaProperties.getSms().getAlphaSenderSupportingCountryCallingCodes().split("\\s"))
		{
			if (targetPhoneNumber.startsWith(prefix))
			{
				return yonaProperties.getSms().getAlphaSenderId();
			}
		}
		return yonaProperties.getSms().getDefaultSenderNumber();
	}

	private HttpPost createHttpRequest(String jsonStr)
	{
		try
		{
			SmsProperties smsProperties = yonaProperties.getSms();
			URI uri = getPlivoUrl(smsProperties);

			HttpPost httpRequest = new HttpPost(uri);
			StringEntity requestEntity = new StringEntity(jsonStr, "UTF-8");

			httpRequest.setEntity(requestEntity);
			httpRequest.setHeader("Accept", "application/json");
			httpRequest.setHeader("Content-type", "application/json");

			return httpRequest;
		}
		catch (URISyntaxException e)
		{
			throw SmsException.smsSendingFailed(e);
		}
	}
}<commit_msg>YD-488 #done Appropriate error code on invalid number (#409)<commit_after>/*******************************************************************************
 * Copyright (c) 2016, 2017 Stichting Yona Foundation This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *******************************************************************************/
package nu.yona.server.sms;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import nu.yona.server.exceptions.InvalidDataException;
import nu.yona.server.exceptions.SmsException;
import nu.yona.server.properties.SmsProperties;
import nu.yona.server.properties.YonaProperties;
import nu.yona.server.util.ThymeleafUtil;

@Service
public class PlivoSmsService implements SmsService
{
	private static final Logger logger = LoggerFactory.getLogger(PlivoSmsService.class);

	@Autowired
	private YonaProperties yonaProperties;

	@Autowired
	@Qualifier("smsTemplateEngine")
	private TemplateEngine templateEngine;

	@Override
	public void send(String phoneNumber, SmsTemplate messageTemplate, Map<String, Object> templateParameters)
	{
		Context ctx = ThymeleafUtil.createContext();
		templateParameters.entrySet().stream().forEach(e -> ctx.setVariable(e.getKey(), e.getValue()));

		String message = templateEngine.process(messageTemplate.getName() + ".txt", ctx);
		logger.info("Sending SMS to number '{}'. Message: {}", phoneNumber, message);

		if (!yonaProperties.getSms().isEnabled())
		{
			logger.info("SMS sending is disabled. No message has been sent.");
			return;
		}

		try (CloseableHttpClient httpClient = HttpClientBuilder.create().build())
		{
			String requestMessageStr = createRequestJson(phoneNumber, message);
			HttpPost httpRequest = createHttpRequest(requestMessageStr);
			HttpClientContext httpClientContext = createHttpClientContext();
			HttpResponse httpResponse = httpClient.execute(httpRequest, httpClientContext);
			HttpEntity entity = httpResponse.getEntity();

			int httpResponseCode = httpResponse.getStatusLine().getStatusCode();
			String httpResponseBody = EntityUtils.toString(entity);
			if (httpResponseCode != HttpStatus.SC_ACCEPTED)
			{
				// @formatter:off
				/*
				 *  In case of an invalid number, the server returns this body:
				 *  {
				 *    "api_id": "d37761b4-b555-11e7-8bc8-065f6a74a84a",
				 *    "error": "'31612345' is not a valid phone number."
				 *  }
				 */
				// @formatter:on
				if (httpResponseBody != null && httpResponseBody.contains("is not a valid phone number"))
				{
					throw InvalidDataException.invalidMobileNumber(phoneNumber);
				}
				throw SmsException.smsSendingFailed(httpResponseCode, httpResponseBody);
			}
		}
		catch (IOException e)
		{
			throw SmsException.smsSendingFailed(e);
		}

		logger.info("SMS sent succesfully.");
	}

	private HttpClientContext createHttpClientContext()
	{
		try
		{
			SmsProperties smsProperties = yonaProperties.getSms();

			URI uri = getPlivoUrl(smsProperties);
			CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
			credentialsProvider.setCredentials(new AuthScope(uri.getHost(), uri.getPort()),
					new UsernamePasswordCredentials(smsProperties.getPlivoAuthId(), smsProperties.getPlivoAuthToken()));

			HttpClientContext httpClientContext = HttpClientContext.create();
			httpClientContext.setCredentialsProvider(credentialsProvider);
			return httpClientContext;
		}
		catch (URISyntaxException e)
		{
			throw SmsException.smsSendingFailed(e);
		}
	}

	private URI getPlivoUrl(SmsProperties smsProperties) throws URISyntaxException
	{
		return new URI(MessageFormat.format(smsProperties.getPlivoUrl(), smsProperties.getPlivoAuthId()));
	}

	private String createRequestJson(String phoneNumber, String message)
	{
		try
		{
			Map<String, Object> requestMessage = new HashMap<>();
			requestMessage.put("src", determineSender(phoneNumber));
			requestMessage.put("dst", phoneNumber.replace("+", ""));
			requestMessage.put("text", message);

			return new ObjectMapper().writeValueAsString(requestMessage);
		}
		catch (JsonProcessingException e)
		{
			throw SmsException.smsSendingFailed(e);
		}
	}

	String determineSender(String targetPhoneNumber)
	{
		for (String prefix : yonaProperties.getSms().getAlphaSenderSupportingCountryCallingCodes().split("\\s"))
		{
			if (targetPhoneNumber.startsWith(prefix))
			{
				return yonaProperties.getSms().getAlphaSenderId();
			}
		}
		return yonaProperties.getSms().getDefaultSenderNumber();
	}

	private HttpPost createHttpRequest(String jsonStr)
	{
		try
		{
			SmsProperties smsProperties = yonaProperties.getSms();
			URI uri = getPlivoUrl(smsProperties);

			HttpPost httpRequest = new HttpPost(uri);
			StringEntity requestEntity = new StringEntity(jsonStr, "UTF-8");

			httpRequest.setEntity(requestEntity);
			httpRequest.setHeader("Accept", "application/json");
			httpRequest.setHeader("Content-type", "application/json");

			return httpRequest;
		}
		catch (URISyntaxException e)
		{
			throw SmsException.smsSendingFailed(e);
		}
	}
}
{'fraction_non_alphanumeric': 0.13040950245036964, 'fraction_numerical': 0.00548218290555694, 'mean_word_length': 5.579234972677596, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 11, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1084893', 'n_tokens_mistral': 3832, 'n_tokens_neox': 3454, 'n_words': 688}