View Javadoc

1   /*
2    * Copyright  2004-2005 Stefan Reuter
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   *
16   */
17  package net.sf.asterisk.manager;
18  
19  import java.io.IOException;
20  import java.io.Serializable;
21  import java.security.MessageDigest;
22  import java.security.NoSuchAlgorithmException;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  
29  import net.sf.asterisk.AsteriskVersion;
30  import net.sf.asterisk.io.SocketConnectionFacade;
31  import net.sf.asterisk.io.impl.SocketConnectionFacadeImpl;
32  import net.sf.asterisk.manager.action.ChallengeAction;
33  import net.sf.asterisk.manager.action.CommandAction;
34  import net.sf.asterisk.manager.action.EventGeneratingAction;
35  import net.sf.asterisk.manager.action.LoginAction;
36  import net.sf.asterisk.manager.action.LogoffAction;
37  import net.sf.asterisk.manager.action.ManagerAction;
38  import net.sf.asterisk.manager.event.ConnectEvent;
39  import net.sf.asterisk.manager.event.DisconnectEvent;
40  import net.sf.asterisk.manager.event.ManagerEvent;
41  import net.sf.asterisk.manager.event.ResponseEvent;
42  import net.sf.asterisk.manager.impl.ManagerReaderImpl;
43  import net.sf.asterisk.manager.impl.ManagerWriterImpl;
44  import net.sf.asterisk.manager.impl.ResponseEventsImpl;
45  import net.sf.asterisk.manager.impl.Util;
46  import net.sf.asterisk.manager.response.ChallengeResponse;
47  import net.sf.asterisk.manager.response.CommandResponse;
48  import net.sf.asterisk.manager.response.ManagerError;
49  import net.sf.asterisk.manager.response.ManagerResponse;
50  import net.sf.asterisk.util.Log;
51  import net.sf.asterisk.util.LogFactory;
52  
53  /***
54   * Default implemention of the ManagerConnection interface.<br>
55   * Generelly avoid direct use of this class. Use the ManagerConnectionFactory to
56   * obtain a ManagerConnection instead.<br>
57   * When using a dependency injection framework like Spring direct usage for
58   * wiring up beans that require a ManagerConnection property is fine though.<br>
59   * Note that the DefaultManagerConnection will create one new Thread
60   * for reading data from Asterisk on the first call to on of the login()
61   * methods.
62   * 
63   * @see net.sf.asterisk.manager.ManagerConnectionFactory
64   * @author srt
65   * @version $Id: DefaultManagerConnection.java,v 1.34 2005/11/08 15:25:18 srt Exp $
66   */
67  public class DefaultManagerConnection implements ManagerConnection, Dispatcher
68  {
69      /***
70       * Instance logger.
71       */
72      private final Log logger = LogFactory.getLog(getClass());
73  
74      /***
75       * Used to construct the internalActionId.
76       */
77      private long actionIdCount = 0;
78  
79      /* Config attributes */
80      /***
81       * The Asterisk server to connect to.
82       */
83      private AsteriskServer asteriskServer;
84  
85      /***
86       * The username to use for login as defined in Asterisk's
87       * <code>manager.conf</code>.
88       */
89      protected String username;
90  
91      /***
92       * The password to use for login as defined in Asterisk's
93       * <code>manager.conf</code>.
94       */
95      protected String password;
96  
97      /***
98       * The default timeout to wait for a ManagerResponse after sending a
99       * ManagerAction.
100      */
101     private long defaultResponseTimeout = 2000;
102 
103     /***
104      * The default timeout to wait for the last ResponseEvent after sending an
105      * EventGeneratingAction.
106      */
107     private long defaultEventTimeout = 5000;
108 
109     /***
110      * The timeout to use when connecting the the Asterisk server.
111      */
112     private int socketTimeout = 0;
113 
114     /***
115      * The time the calling thread is sleeping between checking if a reponse or
116      * the protocol identifer has been received.
117      */
118     private long sleepTime = 50;
119 
120     /***
121      * Should we continue to reconnect after an authentication failure?
122      */
123     private boolean keepAliveAfterAuthenticationFailure = false;
124 
125     /***
126      * The socket to use for TCP/IP communication with Asterisk.
127      */
128     private SocketConnectionFacade socket;
129 
130     /***
131      * The thread that runs the reader.
132      */
133     private Thread readerThread;
134 
135     /***
136      * The reader to use to receive events and responses from asterisk.
137      */
138     private ManagerReader reader;
139 
140     /***
141      * The writer to use to send actions to asterisk.
142      */
143     private ManagerWriter writer;
144 
145     /***
146      * The protocol identifer Asterisk sends on connect.
147      */
148     private String protocolIdentifier;
149     
150     /***
151      * The version of the Asterisk server we are connected to.
152      */
153     private AsteriskVersion version;
154 
155     /***
156      * Contains the registered handlers that process the ManagerResponses.<br>
157      * Key is the internalActionId of the Action sent and value the
158      * corresponding ResponseHandler.
159      */
160     private final Map responseHandlers;
161 
162     /***
163      * Contains the event handlers that handle ResponseEvents for the
164      * sendEventGeneratingAction methods.<br>
165      * Key is the internalActionId of the Action sent and value the
166      * corresponding EventHandler.
167      */
168     private final Map responseEventHandlers;
169 
170     /***
171      * Contains the event handlers that users registered.
172      */
173     private final List eventHandlers;
174 
175     /***
176      * Should we attempt to reconnect when the connection is lost?<br>
177      * This is set to <code>true</code> after successful login and to
178      * <code>false</code> after logoff or after an authentication failure when
179      * keepAliveAfterAuthenticationFailure is <code>false</code>.
180      */
181     protected boolean keepAlive = false;
182 
183     /***
184      * Creates a new instance.
185      */
186     public DefaultManagerConnection()
187     {
188         this.asteriskServer = new AsteriskServer();
189 
190         this.responseHandlers = new HashMap();
191         this.responseEventHandlers = new HashMap();
192         this.eventHandlers = new ArrayList();
193     }
194 
195     /***
196      * Creates a new instance with the given connection parameters.
197      * 
198      * @param hostname the hosname of the Asterisk server to connect to.
199      * @param port the port where Asterisk listens for incoming Manager API
200      *            connections, usually 5038.
201      * @param username the username to use for login
202      * @param password the password to use for login
203      */
204     public DefaultManagerConnection(String hostname, int port, String username,
205             String password)
206     {
207         this();
208 
209         setHostname(hostname);
210         setPort(port);
211         setUsername(username);
212         setPassword(password);
213     }
214 
215     // the following two methods can be overriden when running test cases to
216     // return a mock object
217     protected ManagerReader createReader(Dispatcher dispatcher,
218             AsteriskServer server)
219     {
220         return new ManagerReaderImpl(dispatcher, server);
221     }
222 
223     protected ManagerWriter createWriter()
224     {
225         return new ManagerWriterImpl();
226     }
227 
228     /***
229      * Sets the hostname of the asterisk server to connect to.<br>
230      * Default is <code>localhost</code>.
231      * 
232      * @param hostname the hostname to connect to
233      */
234     public void setHostname(String hostname)
235     {
236         this.asteriskServer.setHostname(hostname);
237     }
238 
239     /***
240      * Sets the port to use to connect to the asterisk server. This is the port
241      * specified in asterisk's <code>manager.conf</code> file.<br>
242      * Default is 5038.
243      * 
244      * @param port the port to connect to
245      */
246     public void setPort(int port)
247     {
248         this.asteriskServer.setPort(port);
249     }
250 
251     /***
252      * Sets the username to use to connect to the asterisk server. This is the
253      * username specified in asterisk's <code>manager.conf</code> file.
254      * 
255      * @param username the username to use for login
256      */
257     public void setUsername(String username)
258     {
259         this.username = username;
260     }
261 
262     /***
263      * Sets the password to use to connect to the asterisk server. This is the
264      * password specified in asterisk's <code>manager.conf</code> file.
265      * 
266      * @param password the password to use for login
267      */
268     public void setPassword(String password)
269     {
270         this.password = password;
271     }
272 
273     /***
274      * Sets the time in milliseconds the synchronous sendAction methods
275      * {@link #sendAction(ManagerAction)} will wait for a response before
276      * throwing a TimeoutException.<br>
277      * Default is 2000.
278      * 
279      * @param defaultTimeout default timeout in milliseconds
280      * @deprecated use {@link #setDefaultResponseTimeout(long)} instead
281      */
282     public void setDefaultTimeout(long defaultTimeout)
283     {
284         setDefaultResponseTimeout(defaultTimeout);
285     }
286 
287     /***
288      * Sets the time in milliseconds the synchronous method
289      * {@link #sendAction(ManagerAction)} will wait for a response before
290      * throwing a TimeoutException.<br>
291      * Default is 2000.
292      * 
293      * @param defaultResponseTimeout default response timeout in milliseconds
294      * @since 0.2
295      */
296     public void setDefaultResponseTimeout(long defaultResponseTimeout)
297     {
298         this.defaultResponseTimeout = defaultResponseTimeout;
299     }
300 
301     /***
302      * Sets the time in milliseconds the synchronous method
303      * {@link #sendEventGeneratingAction(EventGeneratingAction)} will wait for a
304      * response and the last response event before throwing a TimeoutException.<br>
305      * Default is 5000.
306      * 
307      * @param defaultEventTimeout default event timeout in milliseconds
308      * @since 0.2
309      */
310     public void setDefaultEventTimeout(long defaultEventTimeout)
311     {
312         this.defaultEventTimeout = defaultEventTimeout;
313     }
314 
315     /***
316      * Sets the time in milliseconds the synchronous methods
317      * {@link #sendAction(ManagerAction)} and
318      * {@link #sendAction(ManagerAction, long)} will sleep between two checks
319      * for the arrival of a response. This value should be rather small.<br>
320      * The sleepTime attribute is also used when checking for the protocol
321      * identifer.<br>
322      * Default is 50.
323      * 
324      * @param sleepTime time in milliseconds to sleep between two checks for the
325      *            arrival of a response or the protocol identifier
326      * @deprecated this has been replaced by an interrupt based response
327      *             checking approach.
328      */
329     public void setSleepTime(long sleepTime)
330     {
331         this.sleepTime = sleepTime;
332     }
333 
334     /***
335      * Set to <code>true</code> to try reconnecting to ther asterisk serve
336      * even if the reconnection attempt threw an AuthenticationFailedException.<br>
337      * Default is <code>false</code>.
338      */
339     public void setKeepAliveAfterAuthenticationFailure(
340             boolean keepAliveAfterAuthenticationFailure)
341     {
342         this.keepAliveAfterAuthenticationFailure = keepAliveAfterAuthenticationFailure;
343     }
344 
345     /* Implementation of ManagerConnection interface */
346 
347     public void registerUserEventClass(Class userEventClass)
348     {
349         if (reader == null)
350         {
351             reader = createReader(this, asteriskServer);
352         }
353 
354         reader.registerEventClass(userEventClass);
355     }
356 
357     public void setSocketTimeout(int socketTimeout)
358     {
359         this.socketTimeout = socketTimeout;
360     }
361 
362     /***
363      * Logs in to the asterisk manager using asterisk's MD5 based
364      * challenge/response protocol. The login is delayed until the protocol
365      * identifier has been received by the reader.
366      * 
367      * @throws AuthenticationFailedException if the username and/or password are
368      *             incorrect
369      * @throws TimeoutException if no response is received within the specified
370      *             timeout period
371      * @see ChallengeAction
372      * @see LoginAction
373      */
374     public void login() throws IOException, AuthenticationFailedException,
375             TimeoutException
376     {
377         login(defaultResponseTimeout);
378     }
379 
380     /***
381      * Does the real login, following the steps outlined below.<br>
382      * <ol>
383      * <li>Connects to the asterisk server by calling {@link #connect()} if not
384      * already connected
385      * <li>Waits until the protocol identifier is received. This is checked
386      * every {@link #sleepTime} ms but not longer than timeout ms in total.
387      * <li>Sends a {@link ChallengeAction} requesting a challenge for authType
388      * MD5.
389      * <li>When the {@link ChallengeResponse} is received a {@link LoginAction}
390      * is sent using the calculated key (MD5 hash of the password appended to
391      * the received challenge).
392      * </ol>
393      * 
394      * @param timeout the maximum time to wait for the protocol identifier (in
395      *            ms)
396      * @throws AuthenticationFailedException if username or password are
397      *             incorrect and the login action returns an error or if the MD5
398      *             hash cannot be computed. The connection is closed in this
399      *             case.
400      * @throws TimeoutException if a timeout occurs either while waiting for the
401      *             protocol identifier or when sending the challenge or login
402      *             action. The connection is closed in this case.
403      */
404     private void login(long timeout) throws IOException,
405             AuthenticationFailedException, TimeoutException
406     {
407         long start;
408         long timeSpent;
409         ChallengeAction challengeAction;
410         ChallengeResponse challengeResponse;
411         String challenge;
412         String key;
413         LoginAction loginAction;
414         ManagerResponse loginResponse;
415 
416         if (socket == null)
417         {
418             connect();
419         }
420 
421         start = System.currentTimeMillis();
422         while (getProtocolIdentifier() == null)
423         {
424             try
425             {
426                 Thread.sleep(sleepTime);
427             }
428             catch (InterruptedException e)
429             {
430             }
431 
432             timeSpent = System.currentTimeMillis() - start;
433             if (getProtocolIdentifier() == null && timeSpent > timeout)
434             {
435                 disconnect();
436                 throw new TimeoutException(
437                         "Timeout waiting for protocol identifier");
438             }
439         }
440 
441         challengeAction = new ChallengeAction("MD5");
442         challengeResponse = (ChallengeResponse) sendAction(challengeAction);
443 
444         challenge = challengeResponse.getChallenge();
445 
446         try
447         {
448             MessageDigest md;
449 
450             md = MessageDigest.getInstance("MD5");
451             if (challenge != null)
452             {
453                 md.update(challenge.getBytes());
454             }
455             if (password != null)
456             {
457                 md.update(password.getBytes());
458             }
459             key = Util.toHexString(md.digest());
460         }
461         catch (NoSuchAlgorithmException ex)
462         {
463             disconnect();
464             throw new AuthenticationFailedException(
465                     "Unable to create login key using MD5 Message Digest", ex);
466         }
467 
468         loginAction = new LoginAction(username, "MD5", key);
469         loginResponse = sendAction(loginAction);
470         if (loginResponse instanceof ManagerError)
471         {
472             disconnect();
473             throw new AuthenticationFailedException(loginResponse.getMessage());
474         }
475 
476         // successfully logged in so assure that we keep trying to reconnect
477         // when disconnected
478         this.keepAlive = true;
479 
480         logger.info("Successfully logged in");
481         
482         this.version = determineVersion();
483         this.writer.setTargetVersion(version);
484         
485         logger.info("Determined Asterisk version: " + version);
486     }
487     
488     protected AsteriskVersion determineVersion() throws IOException, TimeoutException
489     {
490         ManagerResponse showVersionFilesResponse;
491 
492         // increase timeout as output is quite large
493         showVersionFilesResponse = sendAction(new CommandAction("show version files"), 
494                 defaultResponseTimeout * 2);
495         if (showVersionFilesResponse instanceof CommandResponse)
496         {
497             List showVersionFilesResult;
498             
499             showVersionFilesResult = ((CommandResponse) showVersionFilesResponse).getResult();
500             if (showVersionFilesResult != null && showVersionFilesResult.size() > 0)
501             {
502                 String line1;
503                 
504                 line1 = (String) showVersionFilesResult.get(0); 
505                 if (line1 != null && line1.startsWith("File"))
506                 {
507                     return AsteriskVersion.ASTERISK_1_2;
508                 }
509             }
510         }
511         
512         return AsteriskVersion.ASTERISK_1_0;
513     }
514 
515     protected synchronized void connect() throws IOException
516     {
517         logger.info("Connecting to " + asteriskServer.getHostname() + " port "
518                 + asteriskServer.getPort());
519 
520         if (this.reader == null)
521         {
522             this.reader = createReader(this, asteriskServer);
523         }
524 
525         if (this.writer == null)
526         {
527             this.writer = createWriter();
528         }
529 
530         this.socket = createSocket();
531 
532         this.reader.setSocket(socket);
533         this.readerThread = new Thread(reader, "ManagerReader");
534         this.readerThread.start();
535 
536         this.writer.setSocket(socket);
537     }
538 
539     protected SocketConnectionFacade createSocket() throws IOException
540     {
541         return new SocketConnectionFacadeImpl(asteriskServer.getHostname(),
542                 asteriskServer.getPort(), socketTimeout);
543     }
544 
545     /***
546      * Returns <code>true</code> if there is a socket connection to the
547      * asterisk server, <code>false</code> otherwise.
548      * 
549      * @return <code>true</code> if there is a socket connection to the
550      *         asterisk server, <code>false</code> otherwise.
551      */
552     public synchronized boolean isConnected()
553     {
554         return socket != null && socket.isConnected(); // JDK 1.4
555         // return socket != null;
556     }
557 
558     /***
559      * Sends a {@link LogoffAction} and disconnects from the server.
560      */
561     public synchronized void logoff() throws IOException, TimeoutException
562     {
563         LogoffAction logoffAction;
564 
565         // stop reconnecting when we got disconnected
566         this.keepAlive = false;
567 
568         logoffAction = new LogoffAction();
569 
570         if (socket != null)
571         {
572             sendAction(logoffAction);
573             disconnect();
574         }
575     }
576 
577     /***
578      * Closes the socket connection.
579      */
580     private synchronized void disconnect()
581     {
582         if (this.socket != null)
583         {
584             logger.info("Closing socket.");
585             try
586             {
587                 this.socket.close();
588             }
589             catch (IOException ex)
590             {
591                 logger.warn("Unable to close socket: " + ex.getMessage());
592             }
593             this.socket = null;
594         }
595     }
596 
597     public ManagerResponse sendAction(ManagerAction action) throws IOException,
598             TimeoutException, IllegalArgumentException, IllegalStateException
599     {
600         return sendAction(action, defaultResponseTimeout);
601     }
602 
603     public ManagerResponse sendAction(ManagerAction action, long timeout)
604             throws IOException, TimeoutException, IllegalArgumentException,
605             IllegalStateException
606     {
607         long start;
608         long timeSpent;
609         ResponseHandlerResult result;
610         ManagerResponseHandler callbackHandler;
611 
612         result = new ResponseHandlerResult();
613         callbackHandler = new DefaultResponseHandler(result, Thread
614                 .currentThread());
615 
616         sendAction(action, callbackHandler);
617 
618         start = System.currentTimeMillis();
619         timeSpent = 0;
620         while (result.getResponse() == null)
621         {
622             try
623             {
624                 Thread.sleep(timeout - timeSpent);
625             }
626             catch (InterruptedException ex)
627             {
628             }
629 
630             // still no response and timed out?
631             timeSpent = System.currentTimeMillis() - start;
632             if (result.getResponse() == null && timeSpent > timeout)
633             {
634                 throw new TimeoutException("Timeout waiting for response to "
635                         + action.getAction());
636             }
637         }
638 
639         return result.getResponse();
640     }
641 
642     public void sendAction(ManagerAction action,
643             ManagerResponseHandler callbackHandler) throws IOException,
644             IllegalArgumentException, IllegalStateException
645     {
646         String internalActionId;
647 
648         if (action == null)
649         {
650             throw new IllegalArgumentException(
651                     "Unable to send action: action is null.");
652         }
653 
654         if (socket == null)
655         {
656             throw new IllegalStateException("Unable to send "
657                     + action.getAction() + " action: not connected.");
658         }
659 
660         internalActionId = createInternalActionId();
661 
662         // if the callbackHandler is null the user is obviously not interested
663         // in the response, thats fine.
664         if (callbackHandler != null)
665         {
666             synchronized (this.responseHandlers)
667             {
668                 this.responseHandlers.put(internalActionId, callbackHandler);
669             }
670         }
671 
672         writer.sendAction(action, internalActionId);
673     }
674 
675     public ResponseEvents sendEventGeneratingAction(EventGeneratingAction action)
676             throws IOException, EventTimeoutException,
677             IllegalArgumentException, IllegalStateException
678     {
679         return sendEventGeneratingAction(action, defaultEventTimeout);
680     }
681 
682     public ResponseEvents sendEventGeneratingAction(
683             EventGeneratingAction action, long timeout) throws IOException,
684             EventTimeoutException, IllegalArgumentException,
685             IllegalStateException
686     {
687         ResponseEventsImpl responseEvents;
688         ResponseEventHandler responseEventHandler;
689         String internalActionId;
690         long start;
691         long timeSpent;
692 
693         if (action == null)
694         {
695             throw new IllegalArgumentException(
696                     "Unable to send action: action is null.");
697         }
698         else if (action.getActionCompleteEventClass() == null)
699         {
700             throw new IllegalArgumentException(
701                     "Unable to send action: actionCompleteEventClass is null.");
702         }
703         else if (!ResponseEvent.class.isAssignableFrom(action
704                 .getActionCompleteEventClass()))
705         {
706             throw new IllegalArgumentException(
707                     "Unable to send action: actionCompleteEventClass is not a ResponseEvent.");
708         }
709 
710         if (socket == null)
711         {
712             throw new IllegalStateException("Unable to send "
713                     + action.getAction() + " action: not connected.");
714         }
715 
716         responseEvents = new ResponseEventsImpl();
717         responseEventHandler = new ResponseEventHandler(responseEvents, action
718                 .getActionCompleteEventClass(), Thread.currentThread());
719 
720         internalActionId = createInternalActionId();
721 
722         // register response handler...
723         synchronized (this.responseHandlers)
724         {
725             this.responseHandlers.put(internalActionId, responseEventHandler);
726         }
727 
728         // ...and event handler.
729         synchronized (this.responseEventHandlers)
730         {
731             this.responseEventHandlers.put(internalActionId,
732                     responseEventHandler);
733         }
734 
735         writer.sendAction(action, internalActionId);
736 
737         // let's wait to see what we get
738         start = System.currentTimeMillis();
739         timeSpent = 0;
740         while (responseEvents.getResponse() == null
741                 || !responseEvents.isComplete())
742         {
743             try
744             {
745                 Thread.sleep(timeout - timeSpent);
746             }
747             catch (InterruptedException ex)
748             {
749             }
750 
751             // still no response or not all events received and timed out?
752             timeSpent = System.currentTimeMillis() - start;
753             if ((responseEvents.getResponse() == null || !responseEvents
754                     .isComplete())
755                     && timeSpent > timeout)
756             {
757                 // clean up
758                 synchronized (this.responseEventHandlers)
759                 {
760                     this.responseEventHandlers.remove(internalActionId);
761                 }
762 
763                 throw new EventTimeoutException(
764                         "Timeout waiting for response or response events to "
765                                 + action.getAction(), responseEvents);
766             }
767         }
768 
769         // remove the event handler (note: the response handler is removed
770         // automatically when the response is received)
771         synchronized (this.responseEventHandlers)
772         {
773             this.responseEventHandlers.remove(internalActionId);
774         }
775 
776         return responseEvents;
777     }
778 
779     /***
780      * Creates a new unique internal action id based on the hash code of this
781      * connection and a sequence.
782      * 
783      * @see Util#addInternalActionId(String, String)
784      * @see Util#getInternalActionId(String)
785      * @see Util#stripInternalActionId(String)
786      */
787     private String createInternalActionId()
788     {
789         final StringBuffer sb;
790 
791         sb = new StringBuffer();
792         sb.append(this.hashCode());
793         sb.append("_");
794         sb.append(this.actionIdCount++);
795 
796         return sb.toString();
797     }
798 
799     public void addEventHandler(final ManagerEventHandler eventHandler)
800     {
801         synchronized (this.eventHandlers)
802         {
803             // only add it if its not already there
804             if (!this.eventHandlers.contains(eventHandler))
805             {
806                 this.eventHandlers.add(eventHandler);
807             }
808         }
809     }
810 
811     public void removeEventHandler(final ManagerEventHandler eventHandler)
812     {
813         synchronized (this.eventHandlers)
814         {
815             if (this.eventHandlers.contains(eventHandler))
816             {
817                 this.eventHandlers.remove(eventHandler);
818             }
819         }
820     }
821 
822     public String getProtocolIdentifier()
823     {
824         return this.protocolIdentifier;
825     }
826 
827     public AsteriskServer getAsteriskServer()
828     {
829         return asteriskServer;
830     }
831 
832     /* Implementation of Dispatcher: callbacks for ManagerReader */
833 
834     /***
835      * This method is called by the reader whenever a {@link ManagerResponse} is
836      * received. The response is dispatched to the associated
837      * {@link ManagerResponseHandler}.
838      * 
839      * @param response the response received by the reader
840      * @see ManagerReader
841      */
842     public void dispatchResponse(ManagerResponse response)
843     {
844         final String actionId;
845         String internalActionId;
846         ManagerResponseHandler responseHandler;
847 
848         // shouldn't happen
849         if (response == null)
850         {
851             logger.error("Unable to dispatch null response");
852             return;
853         }
854 
855         actionId = response.getActionId();
856         internalActionId = null;
857         responseHandler = null;
858 
859         if (actionId != null)
860         {
861             internalActionId = Util.getInternalActionId(actionId);
862             response.setActionId(Util.stripInternalActionId(actionId));
863         }
864 
865         logger.debug("Dispatching response with internalActionId '"
866                 + internalActionId + "':\n" + response);
867 
868         if (internalActionId != null)
869         {
870             synchronized (this.responseHandlers)
871             {
872                 responseHandler = (ManagerResponseHandler) this.responseHandlers
873                         .get(internalActionId);
874                 if (responseHandler != null)
875                 {
876                     this.responseHandlers.remove(internalActionId);
877                 }
878                 else
879                 {
880                     // when using the async sendAction it's ok not to register a
881                     // callback so if we don't find a response handler thats ok
882                     logger.debug("No response handler registered for "
883                             + "internalActionId '" + internalActionId + "'");
884                 }
885             }
886         }
887         else
888         {
889             logger.error("Unable to retrieve internalActionId from response: "
890                     + "actionId '" + actionId + "':\n" + response);
891         }
892 
893         if (responseHandler != null)
894         {
895             try
896             {
897                 responseHandler.handleResponse(response);
898             }
899             catch (RuntimeException e)
900             {
901                 logger.warn("Unexpected exception in responseHandler "
902                         + responseHandler.getClass().getName(), e);
903             }
904         }
905     }
906 
907     /***
908      * This method is called by the reader whenever a ManagerEvent is received.
909      * The event is dispatched to all registered ManagerEventHandlers.
910      * 
911      * @param event the event received by the reader
912      * @see #addEventHandler(ManagerEventHandler)
913      * @see #removeEventHandler(ManagerEventHandler)
914      * @see ManagerReader
915      */
916     public void dispatchEvent(ManagerEvent event)
917     {
918         // shouldn't happen
919         if (event == null)
920         {
921             logger.error("Unable to dispatch null event");
922             return;
923         }
924 
925         logger.debug("Dispatching event:\n" + event.toString());
926 
927         // dispatch ResponseEvents to the appropriate responseEventHandler
928         if (event instanceof ResponseEvent)
929         {
930             ResponseEvent responseEvent;
931             String internalActionId;
932 
933             responseEvent = (ResponseEvent) event;
934             internalActionId = responseEvent.getInternalActionId();
935             if (internalActionId != null)
936             {
937                 synchronized (responseEventHandlers)
938                 {
939                     ManagerEventHandler eventHandler;
940 
941                     eventHandler = (ManagerEventHandler) responseEventHandlers
942                             .get(internalActionId);
943                     if (eventHandler != null)
944                     {
945                         try
946                         {
947                             eventHandler.handleEvent(event);
948                         }
949                         catch (RuntimeException e)
950                         {
951                             logger.warn("Unexpected exception in eventHandler "
952                                     + eventHandler.getClass().getName(), e);
953                         }
954                     }
955                 }
956             }
957             else
958             {
959                 logger.error("Unable to handle ResponseEvent without "
960                         + "internalActionId:\n" + responseEvent);
961             }
962         }
963 
964         // dispatch to eventHandlers registered by users
965         synchronized (eventHandlers)
966         {
967             Iterator i = eventHandlers.iterator();
968             while (i.hasNext())
969             {
970                 ManagerEventHandler eventHandler;
971 
972                 eventHandler = (ManagerEventHandler) i.next();
973                 try
974                 {
975                     eventHandler.handleEvent(event);
976                 }
977                 catch (RuntimeException e)
978                 {
979                     logger.warn("Unexpected exception in eventHandler "
980                             + eventHandler.getClass().getName(), e);
981                 }
982             }
983         }
984 
985         // process special events
986         if (event instanceof ConnectEvent)
987         {
988             ConnectEvent connectEvent;
989             String protocolIdentifier;
990 
991             connectEvent = (ConnectEvent) event;
992             protocolIdentifier = connectEvent.getProtocolIdentifier();
993             setProtocolIdentifier(protocolIdentifier);
994         }
995         else if (event instanceof DisconnectEvent)
996         {
997             reconnect();
998         }
999     }
1000 
1001     /***
1002      * This method is called when a {@link ConnectEvent} is received from the
1003      * reader. Having received a correct protocol identifier is the precodition
1004      * for logging in.
1005      * 
1006      * @param protocolIdentifier the protocol version used by the Asterisk
1007      *            server.
1008      */
1009     private void setProtocolIdentifier(final String protocolIdentifier)
1010     {
1011         logger.info("Connected via " + protocolIdentifier);
1012 
1013         if (!"Asterisk Call Manager/1.0".equals(protocolIdentifier))
1014         {
1015             logger.warn("Unsupported protocol version '" + protocolIdentifier
1016                     + "'. Use at your own risk.");
1017         }
1018 
1019         this.protocolIdentifier = protocolIdentifier;
1020 
1021     }
1022 
1023     /***
1024      * Reconnects to the asterisk server when the connection is lost.<br>
1025      * While keepAlive is <code>true</code> we will try to reconnect.
1026      * Reconnection attempts will be stopped when the {@link #logoff()} method
1027      * is called or when the login after a successful reconnect results in an
1028      * {@link AuthenticationFailedException} suggesting that the manager
1029      * credentials have changed and keepAliveAfterAuthenticationFailure is not
1030      * set.<br>
1031      * This method is called when a {@link DisconnectEvent} is received from the
1032      * reader.
1033      */
1034     private void reconnect()
1035     {
1036         int numTries;
1037 
1038         // clean up at first
1039         disconnect();
1040 
1041         // try to reconnect
1042         numTries = 0;
1043         while (this.keepAlive)
1044         {
1045             try
1046             {
1047                 if (numTries < 10)
1048                 {
1049                     // try to reconnect quite fast for the firt 10 times
1050                     // this succeeds if the server has just been restarted
1051                     Thread.sleep(50);
1052                 }
1053                 else
1054                 {
1055                     // slow down after 10 unsuccessful attempts asuming a
1056                     // shutdown of the server
1057                     Thread.sleep(5000);
1058                 }
1059             }
1060             catch (InterruptedException e1)
1061             {
1062                 // it's ok to wake us
1063             }
1064 
1065             try
1066             {
1067                 connect();
1068 
1069                 try
1070                 {
1071                     login();
1072                     logger.info("Successfully reconnected.");
1073                     // everything is ok again, so we leave
1074                     break;
1075                 }
1076                 catch (AuthenticationFailedException e1)
1077                 {
1078                     if (this.keepAliveAfterAuthenticationFailure)
1079                     {
1080                         logger.error("Unable to log in after reconnect.");
1081                     }
1082                     else
1083                     {
1084                         logger.error("Unable to log in after reconnect. "
1085                                 + "Giving up.");
1086                         this.keepAlive = false;
1087                     }
1088                 }
1089                 catch (TimeoutException e1)
1090                 {
1091                     // shouldn't happen
1092                     logger.error("TimeoutException while trying to log in "
1093                             + "after reconnect.");
1094                     synchronized (this)
1095                     {
1096                         socket.close();
1097                     }
1098                 }
1099             }
1100             catch (IOException e)
1101             {
1102                 // server seems to be still down, just continue to attempt
1103                 // reconnection
1104                 logger.warn("Exception while trying to reconnect: "
1105                         + e.getMessage());
1106             }
1107             numTries++;
1108         }
1109     }
1110 
1111     /* Helper classes */
1112 
1113     /***
1114      * A simple data object to store a ManagerResult.
1115      */
1116     private class ResponseHandlerResult implements Serializable
1117     {
1118         /***
1119          * Serializable version identifier
1120          */
1121         private static final long serialVersionUID = 7831097958568769220L;
1122         private ManagerResponse response;
1123 
1124         public ResponseHandlerResult()
1125         {
1126         }
1127 
1128         public ManagerResponse getResponse()
1129         {
1130             return this.response;
1131         }
1132 
1133         public void setResponse(ManagerResponse response)
1134         {
1135             this.response = response;
1136         }
1137     }
1138 
1139     /***
1140      * A simple response handler that stores the received response in a
1141      * ResponseHandlerResult for further processing.
1142      */
1143     private class DefaultResponseHandler
1144             implements
1145                 ManagerResponseHandler,
1146                 Serializable
1147     {
1148         /***
1149          * Serializable version identifier
1150          */
1151         private static final long serialVersionUID = 2926598671855316803L;
1152         private ResponseHandlerResult result;
1153         private final Thread thread;
1154 
1155         /***
1156          * Creates a new instance.
1157          * 
1158          * @param result the result to store the response in
1159          * @param thread the thread to interrupt when the response has been
1160          *            received
1161          */
1162         public DefaultResponseHandler(ResponseHandlerResult result,
1163                 Thread thread)
1164         {
1165             this.result = result;
1166             this.thread = thread;
1167         }
1168 
1169         public void handleResponse(ManagerResponse response)
1170         {
1171             result.setResponse(response);
1172             thread.interrupt();
1173         }
1174     }
1175 
1176     /***
1177      * A combinded event and response handler that adds received events and the
1178      * response to a ResponseEvents object.
1179      */
1180     private class ResponseEventHandler
1181             implements
1182                 ManagerEventHandler,
1183                 ManagerResponseHandler,
1184                 Serializable
1185     {
1186         /***
1187          * Serializable version identifier
1188          */
1189         private static final long serialVersionUID = 2926598671855316803L;
1190         private final ResponseEventsImpl events;
1191         private final Class actionCompleteEventClass;
1192         private final Thread thread;
1193 
1194         /***
1195          * Creates a new instance.
1196          * 
1197          * @param events the ResponseEventsImpl to store the events in
1198          * @param actionCompleteEventClass the type of event that indicates that
1199          *            all events have been received
1200          * @param thread the thread to interrupt when the
1201          *            actionCompleteEventClass has been received
1202          */
1203         public ResponseEventHandler(ResponseEventsImpl events,
1204                 Class actionCompleteEventClass, Thread thread)
1205         {
1206             this.events = events;
1207             this.actionCompleteEventClass = actionCompleteEventClass;
1208             this.thread = thread;
1209         }
1210 
1211         public void handleEvent(ManagerEvent event)
1212         {
1213             // should always be a ResponseEvent, anyway...
1214             if (event instanceof ResponseEvent)
1215             {
1216                 ResponseEvent responseEvent;
1217 
1218                 responseEvent = (ResponseEvent) event;
1219                 events.addEvent(responseEvent);
1220             }
1221 
1222             // finished?
1223             if (actionCompleteEventClass.isAssignableFrom(event.getClass()))
1224             {
1225                 synchronized (events)
1226                 {
1227                     events.setComplete(true);
1228                     if (events.getResponse() != null)
1229                     {
1230                         thread.interrupt();
1231                     }
1232                 }
1233             }
1234         }
1235 
1236         public void handleResponse(ManagerResponse response)
1237         {
1238             synchronized (events)
1239             {
1240                 events.setRepsonse(response);
1241                 if (response instanceof ManagerError)
1242                 {
1243                     events.setComplete(true);
1244                 }
1245 
1246                 // finished?
1247                 if (events.isComplete())
1248                 {
1249                     thread.interrupt();
1250                 }
1251             }
1252         }
1253     }
1254 }