1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 package org.marre.sms.transport.clickatell;
36
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import java.io.BufferedReader;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.InputStreamReader;
44 import java.io.PrintWriter;
45 import java.net.URL;
46 import java.net.URLConnection;
47 import java.net.URLEncoder;
48 import java.text.MessageFormat;
49 import java.text.ParseException;
50 import java.util.LinkedList;
51 import java.util.List;
52 import java.util.Properties;
53
54 import org.marre.sms.SmsAddress;
55 import org.marre.sms.SmsConcatMessage;
56 import org.marre.sms.SmsConstants;
57 import org.marre.sms.SmsDcs;
58 import org.marre.sms.SmsException;
59 import org.marre.sms.SmsMessage;
60 import org.marre.sms.SmsPdu;
61 import org.marre.sms.SmsPduUtil;
62 import org.marre.sms.SmsUdhElement;
63 import org.marre.sms.SmsUdhUtil;
64 import org.marre.sms.SmsUserData;
65 import org.marre.sms.transport.SmsTransport;
66 import org.marre.util.StringUtil;
67
68 /***
69 * An SmsTransport that sends the SMS with clickatell over HTTP.
70 * <p>
71 * It is developed to use the "Clickatell HTTP API v. 2.2.4".
72 * <p>
73 *
74 * Known limitations:<br>
75 * - Cannot send 8-Bit messages without an UDH.<br>
76 * - DCS is not supported. Only UCS2, 7bit, 8bit and SMS class 0 or 1.<br>
77 * - Cannot set validity period (not implemented)<br>
78 * - Doesn't acknowledge the TON or NPI, everything is sent as NPI_ISDN_TELEPHONE and TON_INTERNATIONAL.<br>
79 *
80 * @author Markus Eriksson
81 * @version $Id: ClickatellTransport.java,v 1.27 2005/11/26 16:37:57 c95men Exp $
82 */
83 public class ClickatellTransport implements SmsTransport
84 {
85 private static Logger log_ = LoggerFactory.getLogger(ClickatellTransport.class);
86
87 private String username_;
88 private String password_;
89 private String apiId_;
90 private String sessionId_;
91 private String protocol_;
92
93 /*** Required feature "Text". Set by default. */
94 public static final int FEAT_TEXT = 0x0001;
95 /*** Required feature "8-bit messaging". Set by default. */
96 public static final int FEAT_8BIT = 0x0002;
97 /*** Required feature "udh (binary)". Set by default. */
98 public static final int FEAT_UDH = 0x0004;
99 /*** Required feature "ucs2/unicode". Set by default. */
100 public static final int FEAT_UCS2 = 0x0008;
101 /*** Required feature "alpha originator (sender id)". */
102 public static final int FEAT_ALPHA = 0x0010;
103 /*** Required feature "numeric originator (sender id)". */
104 public static final int FEAT_NUMBER = 0x0020;
105 /*** Required feature "reply to an mt message with a numeric sender id". */
106 public static final int FEAT_REPLY = 0x0040;
107 /*** Required feature "Flash messaging". */
108 public static final int FEAT_FLASH = 0x0200;
109 /*** Required feature "Delivery acknowledgements". */
110 public static final int FEAT_DELIVACK = 0x2000;
111 /*** Required feature "Concatenation". Set by default. */
112 public static final int FEAT_CONCAT = 0x4000;
113 /*** The default required features as explained in HTTP API v224. */
114 public static final int FEAT_DEFAULT = 0x400F;
115
116 /***
117 * Sends a request to clickatell.
118 *
119 * @param url the url to clickatell
120 * @param requestString parameters to send
121 * @return An array of responses (sessionid or msgid)
122 * @throws ClickatellException
123 * @throws IOException
124 */
125 private String[] sendRequest(String url, String requestString) throws ClickatellException, IOException
126 {
127 String response = null;
128 MessageFormat responseFormat = new MessageFormat("{0}: {1}");
129
130 List idList = new LinkedList();
131
132
133
134
135 try
136 {
137 log_.info("sendRequest: posting : " + requestString + " to " + url);
138
139 URL requestURL = new URL(url);
140 URLConnection urlConn = requestURL.openConnection();
141 urlConn.setDoInput(true);
142 urlConn.setDoOutput(true);
143 urlConn.setUseCaches(false);
144 urlConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
145
146
147 PrintWriter pw = new PrintWriter(urlConn.getOutputStream());
148 pw.print(requestString);
149 pw.flush();
150 pw.close();
151
152
153 InputStream is = urlConn.getInputStream();
154 BufferedReader responseReader = new BufferedReader(new InputStreamReader(is));
155
156
157 while ((response = responseReader.readLine()) != null)
158 {
159
160 Object[] objs = responseFormat.parse(response);
161 if ("ERR".equalsIgnoreCase((String) objs[0]))
162 {
163 MessageFormat errorFormat = new MessageFormat("{0}: {1}, {2}");
164 Object[] errObjs = errorFormat.parse(response);
165
166
167 String errorNo = (String) errObjs[1];
168 String description = (String) errObjs[2];
169 throw new ClickatellException("Clickatell error. Error " + errorNo + ", " + description,
170 Integer.parseInt(errorNo));
171 }
172 else
173 {
174 log_.info("sendRequest: Got ID : " + ((String) objs[1]));
175 idList.add((String) objs[1]);
176 }
177 }
178 responseReader.close();
179 }
180 catch (ParseException ex)
181 {
182 throw new ClickatellException("Unexpected response from Clickatell. : " + response,
183 ClickatellException.ERROR_UNKNOWN);
184 }
185
186 return (String[]) idList.toArray(new String[0]);
187 }
188
189 private String[] sendRequestWithRetry(String url, String requestString)
190 throws SmsException, IOException
191 {
192 String[] msgIds;
193
194
195 try
196 {
197 msgIds = sendRequest(url, requestString);
198 }
199 catch (ClickatellException ex)
200 {
201 switch (ex.getErrId())
202 {
203
204
205 case ClickatellException.ERROR_AUTH_FAILED:
206 case ClickatellException.ERROR_SESSION_ID_EXPIRED:
207
208 connect();
209
210
211
212 try
213 {
214 msgIds = sendRequest(url, requestString);
215 }
216 catch (ClickatellException ex2)
217 {
218 throw new SmsException(ex2.getMessage());
219 }
220 break;
221
222 case ClickatellException.ERROR_UNKNOWN:
223 default:
224 throw new SmsException(ex.getMessage());
225 }
226 }
227
228 return msgIds;
229 }
230
231 /***
232 * Initializes the transport.
233 * <p>
234 * It expects the following properties in theProps param:
235 *
236 * <pre>
237 * smsj.clickatell.username - clickatell username
238 * smsj.clickatell.password - clickatell password
239 * smsj.clickatell.apiid - clickatell apiid
240 * smsj.clickatell.protocol - http or https
241 * </pre>
242 *
243 * @param props
244 * Properties to initialize the library
245 * @throws SmsException
246 * If not given the needed params
247 */
248 public void init(Properties props) throws SmsException
249 {
250 username_ = props.getProperty("smsj.clickatell.username");
251 password_ = props.getProperty("smsj.clickatell.password");
252 apiId_ = props.getProperty("smsj.clickatell.apiid");
253 protocol_ = props.getProperty("smsj.clickatell.protocol", "http");
254
255 if ((username_ == null) || (password_ == null) || (apiId_ == null))
256 {
257 throw new SmsException("Incomplete login information for clickatell");
258 }
259
260 if (! (protocol_.equals("http") || protocol_.equals("https")))
261 {
262 throw new SmsException("Unsupported protocol : " + protocol_);
263 }
264 }
265
266 /***
267 * Sends an auth command to clickatell to get an session id that can be used
268 * later.
269 * @throws SmsException
270 * If we fail to authenticate to clickatell or if we fail to
271 * connect.
272 * @throws IOException
273 */
274 public void connect() throws SmsException, IOException
275 {
276 String[] response = null;
277 String url = protocol_ + "://api.clickatell.com/http/auth";
278 String requestString;
279
280 requestString = "api_id=" + apiId_;
281 requestString += "&user=" + username_;
282 requestString += "&password=" + password_;
283
284 try
285 {
286 response = sendRequest(url, requestString);
287 }
288 catch (ClickatellException ex)
289 {
290 throw new SmsException(ex);
291 }
292
293 sessionId_ = response[0];
294 }
295
296 /***
297 *
298 */
299 private String buildSendRequest(SmsUserData ud, byte[] udhData, SmsAddress dest, SmsAddress sender)
300 throws SmsException
301 {
302 String requestString;
303 int reqFeat = 0;
304
305 requestString = "session_id=" + sessionId_;
306 requestString += "&to=" + dest.getAddress();
307
308 if (SmsUdhUtil.isConcat(ud, udhData))
309 {
310 requestString += "&concat=3";
311 reqFeat |= FEAT_CONCAT;
312 }
313
314 if (sender != null)
315 {
316 requestString += "&from=" + sender.getAddress();
317 reqFeat |= (sender.getTypeOfNumber() == SmsConstants.TON_ALPHANUMERIC) ? FEAT_ALPHA : FEAT_NUMBER;
318 }
319
320
321 if (ud.getDcs().getMessageClass() == SmsDcs.MSG_CLASS_0)
322 {
323 requestString += "&msg_type=SMS_FLASH";
324 reqFeat |= FEAT_FLASH;
325 }
326
327
328
329
330 if ( (udhData == null) || (udhData.length == 0) )
331 {
332
333
334
335 switch (ud.getDcs().getAlphabet())
336 {
337 case SmsDcs.ALPHABET_8BIT:
338 throw new SmsException("Clickatell API cannot send 8 bit encoded messages without UDH");
339
340 case SmsDcs.ALPHABET_UCS2:
341 String udStr = StringUtil.bytesToHexString(ud.getData());
342 requestString += "&unicode=1";
343 requestString += "&text=" + udStr;
344 reqFeat |= FEAT_UCS2;
345 break;
346
347 case SmsDcs.ALPHABET_GSM:
348 String msg = SmsPduUtil.readSeptets(ud.getData(), ud.getLength());
349 requestString += "&text=" + URLEncoder.encode(msg);
350 reqFeat |= FEAT_TEXT;
351 break;
352
353 default:
354 throw new SmsException("Unsupported data coding scheme");
355 }
356 }
357 else
358 {
359 String udStr;
360 String udhStr;
361
362
363
364
365 switch (ud.getDcs().getAlphabet())
366 {
367 case SmsDcs.ALPHABET_8BIT:
368 udStr = StringUtil.bytesToHexString(ud.getData());
369 udhStr = StringUtil.bytesToHexString(udhData);
370 requestString += "&udh=" + udhStr;
371 requestString += "&text=" + udStr;
372 reqFeat |= FEAT_UDH | FEAT_8BIT;
373 break;
374
375 case SmsDcs.ALPHABET_UCS2:
376 udStr = StringUtil.bytesToHexString(ud.getData());
377 udhStr = StringUtil.bytesToHexString(udhData);
378 requestString += "&unicode=1";
379 requestString += "&udh=" + udhStr;
380 requestString += "&text=" + udStr;
381 reqFeat |= FEAT_UDH | FEAT_UCS2;
382 break;
383
384 case SmsDcs.ALPHABET_GSM:
385 throw new SmsException("Clickatell API cannot send 7 bit encoded messages with UDH");
386
387 default:
388 throw new SmsException("Unsupported data coding scheme");
389 }
390 }
391
392
393 requestString += "&req_feat=" + reqFeat;
394
395 return requestString;
396 }
397
398 /***
399 * More effective sending of SMS.
400 *
401 * @param theMsg
402 * @param theDestination
403 * @param theSender
404 * @throws SmsException
405 */
406 private String[] sendConcatMessage(SmsConcatMessage theMsg, SmsAddress theDestination, SmsAddress theSender)
407 throws SmsException, IOException
408 {
409 String url = protocol_ + "://api.clickatell.com/http/sendmsg";
410 SmsUserData userData = theMsg.getUserData();
411 SmsUdhElement[] udhElements = theMsg.getUdhElements();
412 byte[] udhData = SmsUdhUtil.toByteArray(udhElements);
413
414 String requestString = buildSendRequest(userData, udhData, theDestination, theSender);
415 return sendRequestWithRetry(url, requestString);
416 }
417
418 /***
419 * Sends an sendmsg command to clickatell.
420 *
421 * @param thePdu
422 * @param theDestination
423 * @param theSender
424 * @throws SmsException
425 * If clickatell sends an error message, unexpected response or
426 * if we fail to connect.
427 */
428 private String send(SmsPdu thePdu, SmsAddress theDestination, SmsAddress theSender) throws SmsException, IOException
429 {
430 String url = protocol_ + "://api.clickatell.com/http/sendmsg";
431 SmsUserData userData = thePdu.getUserData();
432 byte[] udhData = thePdu.getUserDataHeaders();
433
434 String requestString = buildSendRequest(userData, udhData, theDestination, theSender);
435
436 return sendRequestWithRetry(url, requestString)[0];
437 }
438
439 /***
440 * Sends an SMS Message.
441 *
442 * @param msg
443 * @param dest
444 * @param sender
445 * @throws SmsException
446 * @return Message ids
447 */
448 public String send(SmsMessage msg, SmsAddress dest, SmsAddress sender) throws SmsException, IOException
449 {
450 String[] msgIds;
451
452 if (dest.getTypeOfNumber() == SmsConstants.TON_ALPHANUMERIC)
453 {
454 throw new SmsException("Cannot sent SMS to an ALPHANUMERIC address");
455 }
456
457 if (sessionId_ == null)
458 {
459 throw new SmsException("Must connect before sending");
460 }
461
462
463 if (msg instanceof SmsConcatMessage)
464 {
465 msgIds = sendConcatMessage((SmsConcatMessage) msg, dest, sender);
466 }
467 else
468 {
469 SmsPdu[] msgPdu = msg.getPdus();
470 msgIds = new String[msgPdu.length];
471
472 for (int i = 0; i < msgPdu.length; i++)
473 {
474 msgIds[i] = send(msgPdu[i], dest, sender);
475 }
476 }
477
478
479 return null;
480 }
481
482 /***
483 * Disconnect from clickatell.
484 *
485 * Not needed for the clickatell API
486 *
487 * @throws SmsException Never
488 * @throws IOException Never
489 */
490 public void disconnect() throws SmsException, IOException
491 {
492
493 }
494
495 /***
496 * Pings the clickatell service
497 *
498 * Not needed for the clickatell API
499 *
500 * @throws SmsException Never
501 * @throws IOException Never
502 */
503 public void ping() throws SmsException, IOException
504 {
505 String[] response = null;
506 String url = protocol_ + "://api.clickatell.com/http/ping";
507 String requestString;
508
509 requestString = "session_id=" + sessionId_;
510
511 try
512 {
513 response = sendRequest(url, requestString);
514 }
515 catch (ClickatellException ex)
516 {
517 throw new SmsException(ex);
518 }
519 }
520 }