OvhSmsSender.java

1
package fr.sii.ogham.sms.sender.impl;
2
3
import static fr.sii.ogham.sms.OvhSmsConstants.DEFAULT_OVHSMS_HTTP2SMS_IMPLEMENTATION_PRIORITY;
4
5
import java.io.IOException;
6
import java.net.URL;
7
import java.util.ArrayList;
8
import java.util.List;
9
import java.util.regex.Pattern;
10
11
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13
14
import com.fasterxml.jackson.core.JsonProcessingException;
15
import com.fasterxml.jackson.databind.JsonNode;
16
import com.fasterxml.jackson.databind.ObjectMapper;
17
18
import fr.sii.ogham.core.builder.priority.Priority;
19
import fr.sii.ogham.core.exception.MessageException;
20
import fr.sii.ogham.core.exception.MessageNotSentException;
21
import fr.sii.ogham.core.exception.util.HttpException;
22
import fr.sii.ogham.core.exception.util.PhoneNumberException;
23
import fr.sii.ogham.core.sender.AbstractSpecializedSender;
24
import fr.sii.ogham.core.util.StringUtils;
25
import fr.sii.ogham.sms.message.PhoneNumber;
26
import fr.sii.ogham.sms.message.Recipient;
27
import fr.sii.ogham.sms.message.Sms;
28
import fr.sii.ogham.sms.sender.impl.ovh.OvhAuthParams;
29
import fr.sii.ogham.sms.sender.impl.ovh.OvhOptions;
30
import fr.sii.ogham.sms.sender.impl.ovh.SmsCoding;
31
import fr.sii.ogham.sms.sender.impl.ovh.SmsCodingDetector;
32
import fr.sii.ogham.sms.util.HttpUtils;
33
import fr.sii.ogham.sms.util.http.Parameter;
34
import fr.sii.ogham.sms.util.http.Response;
35
36
/**
37
 * Implementation that is able to send SMS through <a href=
38
 * "https://docs.ovh.com/fr/sms/envoyer_des_sms_depuis_une_url_-_http2sms/#objectif">OVH
39
 * web service</a>. This sender requires that phone numbers are provided using
40
 * either:
41
 * <ul>
42
 * <li><a href="https://en.wikipedia.org/wiki/E.164">E.164 international
43
 * format</a> (prefixed with '+' followed by country code)</li>
44
 * <li><a href="https://en.wikipedia.org/wiki/E.123">E.123 international
45
 * format</a> (prefixed with '+' followed by country code and can contain
46
 * spaces)</li>
47
 * <li>Using 13 digits: country code on 4 digits followed by number. For
48
 * example, 0033 6 01 02 03 04 is a valid French number (country code is 33,
49
 * additional '0' are added to reach the 4 digits)</li>
50
 * </ul>
51
 * 
52
 * @author Aurélien Baudet
53
 *
54
 */
55
@Priority(properties = "${ogham.sms.implementation-priority.ovh-http2sms}", defaultValue = DEFAULT_OVHSMS_HTTP2SMS_IMPLEMENTATION_PRIORITY)
56
public class OvhSmsSender extends AbstractSpecializedSender<Sms> {
57
	private static final Logger LOG = LoggerFactory.getLogger(OvhSmsSender.class);
58
	private static final String CONTENT_TYPE = "application/json";
59
	private static final String RESPONSE_TYPE = "contentType";
60
	private static final String MESSAGE = "message";
61
	private static final String SMS_CODING = "smsCoding";
62
	private static final String TO = "to";
63
	private static final String FROM = "from";
64
	private static final String RECIPIENTS_SEPARATOR = ",";
65
	private static final int OK_STATUS = 200;
66
	private static final int INTERNATIONAL_FORMAT_LENGTH = 13;
67
	private static final Pattern SPACES = Pattern.compile("\\s+");
68
69
	/**
70
	 * The authentication parameters
71
	 */
72
	private final OvhAuthParams authParams;
73
74
	/**
75
	 * The OVH options
76
	 */
77
	private final OvhOptions options;
78
79
	/**
80
	 * This is used to parse JSON response
81
	 */
82
	private final ObjectMapper mapper;
83
84
	/**
85
	 * The URL to OVH web service
86
	 */
87
	private final URL url;
88
89
	/**
90
	 * If {@link SmsCoding} not set, detects which {@link SmsCoding} can be used
91
	 * to encode the message
92
	 */
93
	private final SmsCodingDetector smsCodingDetector;
94
95
	public OvhSmsSender(URL url, OvhAuthParams authParams, OvhOptions options, SmsCodingDetector smsCodingDetector) {
96
		super();
97
		this.url = url;
98
		this.authParams = authParams;
99
		this.options = options;
100
		this.smsCodingDetector = smsCodingDetector;
101
		this.mapper = new ObjectMapper();
102
	}
103
104
	@Override
105
	public void send(Sms message) throws MessageException {
106
		try {
107
			String text = getContent(message);
108
			// @formatter:off
109
			Response response = HttpUtils.get(url.toString(), authParams, options,
110
									new Parameter(SMS_CODING, getSmsCodingValue(text)),
111
									new Parameter(RESPONSE_TYPE, CONTENT_TYPE),
112
									// convert phone number to international format
113
									new Parameter(FROM, toInternational(message.getFrom().getPhoneNumber())),
114
									new Parameter(TO, StringUtils.join(convert(message.getRecipients()), RECIPIENTS_SEPARATOR)),
115
									// TODO: manage long messages: how to do ??
116
									new Parameter(MESSAGE, text));
117
			// @formatter:on
118 2 1. send : removed call to fr/sii/ogham/sms/sender/impl/OvhSmsSender::handleResponse → NO_COVERAGE
2. send : removed call to fr/sii/ogham/sms/sender/impl/OvhSmsSender::handleResponse → TIMED_OUT
			handleResponse(message, response);
119
		} catch (IOException e) {
120
			throw new MessageException("Failed to read response when sending SMS through OVH", message, e);
121
		} catch (HttpException e) {
122
			throw new MessageException("Failed to send SMS through OVH", message, e);
123
		} catch (PhoneNumberException e) {
124
			throw new MessageException("Failed to send SMS through OVH (invalid phone number)", message, e);
125
		}
126
	}
127
128
	private Integer getSmsCodingValue(String message) {
129
		SmsCoding smsCoding = getSmsCoding(message);
130 4 1. getSmsCodingValue : replaced Integer return value with 0 for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCodingValue → NO_COVERAGE
2. getSmsCodingValue : negated conditional → NO_COVERAGE
3. getSmsCodingValue : replaced Integer return value with 0 for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCodingValue → KILLED
4. getSmsCodingValue : negated conditional → KILLED
		return smsCoding == null ? null : smsCoding.getValue();
131
	}
132
133
	private SmsCoding getSmsCoding(String message) {
134 2 1. getSmsCoding : negated conditional → NO_COVERAGE
2. getSmsCoding : negated conditional → KILLED
		if (options.getSmsCoding() != null) {
135 2 1. getSmsCoding : replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → NO_COVERAGE
2. getSmsCoding : replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → KILLED
			return options.getSmsCoding();
136
		}
137 2 1. getSmsCoding : replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → NO_COVERAGE
2. getSmsCoding : replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → KILLED
		return smsCodingDetector.detect(message);
138
	}
139
140
	/**
141
	 * Handle OVH response. If status provided in response is less than 200,
142
	 * then the message has been sent. Otherwise, the message has not been sent.
143
	 * 
144
	 * @param message
145
	 *            the SMS to send
146
	 * @param response
147
	 *            the received response from OVH API
148
	 * @throws IOException
149
	 *             when the response couldn't be read
150
	 * @throws JsonProcessingException
151
	 *             when the response format is not valid JSON
152
	 * @throws MessageNotSentException
153
	 *             generated exception to indicate that the message couldn't be
154
	 *             sent
155
	 */
156
	private void handleResponse(Sms message, Response response) throws IOException, MessageNotSentException {
157 2 1. handleResponse : negated conditional → NO_COVERAGE
2. handleResponse : negated conditional → KILLED
		if (response.getStatus().isSuccess()) {
158
			JsonNode json = mapper.readTree(response.getBody());
159
			int ovhStatus = json.get("status").asInt();
160
			// 100 <= ovh status < 200 ====> OK -> just log response
161
			// 200 <= ovh status ====> KO -> throw an exception
162 4 1. handleResponse : changed conditional boundary → NO_COVERAGE
2. handleResponse : negated conditional → NO_COVERAGE
3. handleResponse : changed conditional boundary → TIMED_OUT
4. handleResponse : negated conditional → KILLED
			if (ovhStatus >= OK_STATUS) {
163
				LOG.error("SMS failed to be sent through OVH");
164
				LOG.debug("Sent SMS: {}", message);
165
				LOG.debug("Response status {}", response.getStatus());
166
				LOG.debug("Response body {}", response.getBody());
167
				throw new MessageNotSentException("SMS couldn't be sent through OVH: " + json.get(MESSAGE).asText(), message);
168
			} else {
169
				LOG.info("SMS successfully sent through OVH");
170
				LOG.debug("Sent SMS: {}", message);
171
				LOG.debug("Response: {}", response.getBody());
172
			}
173
		} else {
174
			LOG.error("Response status {}", response.getStatus());
175
			LOG.error("Response body {}", response.getBody());
176
			throw new MessageNotSentException("SMS couldn't be sent. Response status is " + response.getStatus(), message);
177
		}
178
	}
179
180
	/**
181
	 * Get the content of the SMS and apply some transformations on it to be
182
	 * usable by OVH.
183
	 * 
184
	 * @param message
185
	 *            the message that contains the content to extract
186
	 * @return the content formatted for OVH
187
	 */
188
	private static String getContent(Sms message) {
189
		// if a string contains \r\n, only \r is kept
190
		// if there are \n without \r, those \n are converted to \r
191 2 1. getContent : replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getContent → NO_COVERAGE
2. getContent : replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getContent → KILLED
		return message.getContent().toString().replaceAll("(\r)?\n", "\r");
192
	}
193
194
	/**
195
	 * Convert the list of SMS recipients to international phone numbers usable
196
	 * by OVH.
197
	 * 
198
	 * @param recipients
199
	 *            the list of recipients
200
	 * @return the list of international phone numbers
201
	 * @throws PhoneNumberException
202
	 *             when phone number can't be handled by OVH
203
	 */
204
	private static List<String> convert(List<Recipient> recipients) throws PhoneNumberException {
205
		List<String> tos = new ArrayList<>(recipients.size());
206
		// convert phone numbers to international format
207
		for (Recipient recipient : recipients) {
208
			tos.add(toInternational(recipient.getPhoneNumber()));
209
		}
210 2 1. convert : replaced return value with Collections.emptyList for fr/sii/ogham/sms/sender/impl/OvhSmsSender::convert → NO_COVERAGE
2. convert : replaced return value with Collections.emptyList for fr/sii/ogham/sms/sender/impl/OvhSmsSender::convert → KILLED
		return tos;
211
	}
212
213
	/**
214
	 * Convert a raw phone number to its international form usable by OVH (13
215
	 * digits with no space).
216
	 * 
217
	 * @param phoneNumber
218
	 *            the phone number to transform
219
	 * @return the international phone number
220
	 * @throws PhoneNumberException
221
	 *             when phone number can't be handled by OVH
222
	 */
223
	private static String toInternational(PhoneNumber phoneNumber) throws PhoneNumberException {
224
		String number = phoneNumber.getNumber();
225 4 1. toInternational : negated conditional → NO_COVERAGE
2. toInternational : negated conditional → NO_COVERAGE
3. toInternational : negated conditional → KILLED
4. toInternational : negated conditional → KILLED
		if (number.startsWith("+") || number.length() == INTERNATIONAL_FORMAT_LENGTH) {
226
			String withoutPlus = number.replace("+", "");
227
			String withoutSpaces = SPACES.matcher(withoutPlus).replaceAll("");
228 2 1. toInternational : replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::toInternational → NO_COVERAGE
2. toInternational : replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::toInternational → KILLED
			return StringUtils.leftPad(withoutSpaces, INTERNATIONAL_FORMAT_LENGTH, '0');
229
		} else {
230
			throw new PhoneNumberException("Invalid phone number. OVH only accepts international phone numbers. Please write the phone number with the country prefix. "
231
					+ "For example, if the number is 0601020304 and it is a French number, then the international number is +33601020304", phoneNumber);
232
		}
233
	}
234
235
	public OvhAuthParams getAuthParams() {
236 1 1. getAuthParams : replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getAuthParams → NO_COVERAGE
		return authParams;
237
	}
238
239
	public OvhOptions getOptions() {
240 1 1. getOptions : replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getOptions → NO_COVERAGE
		return options;
241
	}
242
243
	public URL getUrl() {
244 1 1. getUrl : replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getUrl → NO_COVERAGE
		return url;
245
	}
246
}

Mutations

118

1.1
Location : send
Killed by : none
removed call to fr/sii/ogham/sms/sender/impl/OvhSmsSender::handleResponse → NO_COVERAGE

2.2
Location : send
Killed by : none
removed call to fr/sii/ogham/sms/sender/impl/OvhSmsSender::handleResponse → TIMED_OUT

130

1.1
Location : getSmsCodingValue
Killed by : none
replaced Integer return value with 0 for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCodingValue → NO_COVERAGE

2.2
Location : getSmsCodingValue
Killed by : oghamovh.it.SmsCodingTest.unicode(oghamovh.it.SmsCodingTest)
replaced Integer return value with 0 for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCodingValue → KILLED

3.3
Location : getSmsCodingValue
Killed by : none
negated conditional → NO_COVERAGE

4.4
Location : getSmsCodingValue
Killed by : oghamovh.it.SmsCodingTest.unicode(oghamovh.it.SmsCodingTest)
negated conditional → KILLED

134

1.1
Location : getSmsCoding
Killed by : oghamovh.it.SmsCodingTest.unicode(oghamovh.it.SmsCodingTest)
negated conditional → KILLED

2.2
Location : getSmsCoding
Killed by : none
negated conditional → NO_COVERAGE

135

1.1
Location : getSmsCoding
Killed by : oghamovh.it.SmsCodingTest.fixedValue(oghamovh.it.SmsCodingTest)
replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → KILLED

2.2
Location : getSmsCoding
Killed by : none
replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → NO_COVERAGE

137

1.1
Location : getSmsCoding
Killed by : oghamovh.it.SmsCodingTest.unicode(oghamovh.it.SmsCodingTest)
replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → KILLED

2.2
Location : getSmsCoding
Killed by : none
replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getSmsCoding → NO_COVERAGE

157

1.1
Location : handleResponse
Killed by : none
negated conditional → NO_COVERAGE

2.2
Location : handleResponse
Killed by : oghamovh.it.OvhSmsTest.phoneNumberConversion(oghamovh.it.OvhSmsTest)
negated conditional → KILLED

162

1.1
Location : handleResponse
Killed by : none
changed conditional boundary → TIMED_OUT

2.2
Location : handleResponse
Killed by : none
changed conditional boundary → NO_COVERAGE

3.3
Location : handleResponse
Killed by : oghamovh.it.OvhSmsTest.phoneNumberConversion(oghamovh.it.OvhSmsTest)
negated conditional → KILLED

4.4
Location : handleResponse
Killed by : none
negated conditional → NO_COVERAGE

191

1.1
Location : getContent
Killed by : none
replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getContent → NO_COVERAGE

2.2
Location : getContent
Killed by : oghamovh.it.OvhSmsTest.phoneNumberConversion(oghamovh.it.OvhSmsTest)
replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getContent → KILLED

210

1.1
Location : convert
Killed by : oghamovh.it.OvhSmsTest.phoneNumberConversion(oghamovh.it.OvhSmsTest)
replaced return value with Collections.emptyList for fr/sii/ogham/sms/sender/impl/OvhSmsSender::convert → KILLED

2.2
Location : convert
Killed by : none
replaced return value with Collections.emptyList for fr/sii/ogham/sms/sender/impl/OvhSmsSender::convert → NO_COVERAGE

225

1.1
Location : toInternational
Killed by : none
negated conditional → NO_COVERAGE

2.2
Location : toInternational
Killed by : oghamovh.it.OvhSmsTest.nationalNumber(oghamovh.it.OvhSmsTest)
negated conditional → KILLED

3.3
Location : toInternational
Killed by : none
negated conditional → NO_COVERAGE

4.4
Location : toInternational
Killed by : oghamovh.it.OvhSmsTest.nationalNumber(oghamovh.it.OvhSmsTest)
negated conditional → KILLED

228

1.1
Location : toInternational
Killed by : oghamovh.it.OvhSmsTest.phoneNumberConversion(oghamovh.it.OvhSmsTest)
replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::toInternational → KILLED

2.2
Location : toInternational
Killed by : none
replaced return value with "" for fr/sii/ogham/sms/sender/impl/OvhSmsSender::toInternational → NO_COVERAGE

236

1.1
Location : getAuthParams
Killed by : none
replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getAuthParams → NO_COVERAGE

240

1.1
Location : getOptions
Killed by : none
replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getOptions → NO_COVERAGE

244

1.1
Location : getUrl
Killed by : none
replaced return value with null for fr/sii/ogham/sms/sender/impl/OvhSmsSender::getUrl → NO_COVERAGE

Active mutators

Tests examined


Report generated by PIT OGHAM