View Javadoc

1   /* ***** BEGIN LICENSE BLOCK *****
2    * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3    *
4    * The contents of this file are subject to the Mozilla Public License Version
5    * 1.1 (the "License"); you may not use this file except in compliance with
6    * the License. You may obtain a copy of the License at
7    * http://www.mozilla.org/MPL/
8    *
9    * Software distributed under the License is distributed on an "AS IS" basis,
10   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11   * for the specific language governing rights and limitations under the
12   * License.
13   *
14   * The Original Code is "SMS Library for the Java platform".
15   *
16   * The Initial Developer of the Original Code is Markus Eriksson.
17   * Portions created by the Initial Developer are Copyright (C) 2002
18   * the Initial Developer. All Rights Reserved.
19   *
20   * Contributor(s):
21   *
22   * Alternatively, the contents of this file may be used under the terms of
23   * either the GNU General Public License Version 2 or later (the "GPL"), or
24   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
25   * in which case the provisions of the GPL or the LGPL are applicable instead
26   * of those above. If you wish to allow use of your version of this file only
27   * under the terms of either the GPL or the LGPL, and not to allow others to
28   * use your version of this file under the terms of the MPL, indicate your
29   * decision by deleting the provisions above and replace them with the notice
30   * and other provisions required by the GPL or the LGPL. If you do not delete
31   * the provisions above, a recipient may use your version of this file under
32   * the terms of any one of the MPL, the GPL or the LGPL.
33   *
34   * ***** END LICENSE BLOCK ***** */
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         // Send request to clickatell
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             // Send request
147             PrintWriter pw = new PrintWriter(urlConn.getOutputStream());
148             pw.print(requestString);                     
149             pw.flush();
150             pw.close();
151             
152             // Connect
153             InputStream is = urlConn.getInputStream(); 
154             BufferedReader responseReader = new BufferedReader(new InputStreamReader(is));
155 
156             // Read response
157             while ((response = responseReader.readLine()) != null)
158             {
159                 // Parse response
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                     // Error message...
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         // Send request to clickatell
195         try
196         {
197             msgIds = sendRequest(url, requestString);
198         }
199         catch (ClickatellException ex)
200         {
201             switch (ex.getErrId())
202             {
203             // 858141 : Clickatell is not sending "Session id expired"
204             // they are using "Authentication failed" instead
205             case ClickatellException.ERROR_AUTH_FAILED:
206             case ClickatellException.ERROR_SESSION_ID_EXPIRED:
207                 // Try to get a new session id
208                 connect();
209     
210                 // Retry the request...
211                 // OK, this is a bit ugly...
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         // CLASS_0 message?
321         if (ud.getDcs().getMessageClass() == SmsDcs.MSG_CLASS_0)
322         {
323             requestString += "&msg_type=SMS_FLASH";
324             reqFeat |= FEAT_FLASH;
325         }
326         
327         //
328         // Generate request URL
329         //
330         if ( (udhData == null) || (udhData.length == 0) )
331         {                        
332             //
333             // Message without UDH
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             // Message Contains UDH
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         // Add the req_feat parameter
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         // TODO: Return a real message id
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         // The clickatell HTTP API is connection less
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 }