View Javadoc

1   // ========================================================================
2   // Copyright 2006-20078 Mort Bay Consulting Pty. Ltd.
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   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.cometd.client;
16  
17  import java.io.IOException;
18  import java.net.URLEncoder;
19  import java.util.ArrayList;
20  import java.util.LinkedList;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Queue;
24  import java.util.Timer;
25  import java.util.TimerTask;
26  import java.util.concurrent.ConcurrentHashMap;
27  
28  import javax.servlet.http.Cookie;
29  
30  import org.cometd.Bayeux;
31  import org.cometd.Client;
32  import org.cometd.ClientListener;
33  import org.cometd.Extension;
34  import org.cometd.Message;
35  import org.cometd.MessageListener;
36  import org.cometd.RemoveListener;
37  import org.mortbay.cometd.MessageImpl;
38  import org.mortbay.cometd.MessagePool;
39  import org.mortbay.component.AbstractLifeCycle;
40  import org.mortbay.io.Buffer;
41  import org.mortbay.io.ByteArrayBuffer;
42  import org.mortbay.jetty.HttpHeaders;
43  import org.mortbay.jetty.HttpSchemes;
44  import org.mortbay.jetty.HttpURI;
45  import org.mortbay.jetty.client.Address;
46  import org.mortbay.jetty.client.ContentExchange;
47  import org.mortbay.jetty.client.HttpClient;
48  import org.mortbay.jetty.client.HttpExchange;
49  import org.mortbay.log.Log;
50  import org.mortbay.util.ArrayQueue;
51  import org.mortbay.util.LazyList;
52  import org.mortbay.util.QuotedStringTokenizer;
53  import org.mortbay.util.ajax.JSON;
54  
55  /* ------------------------------------------------------------ */
56  /**
57   * Bayeux protocol Client.
58   * <p>
59   * Implements a Bayeux Ajax Push client as part of the cometd project.
60   * <p>
61   * The HttpClient attributes are used to share a Timer and MessagePool instance
62   * between all Bayeux clients sharing the same HttpClient.
63   * 
64   * @see http://cometd.org
65   * @author gregw
66   * 
67   */
68  public class BayeuxClient extends AbstractLifeCycle implements Client
69  {
70      private final static String __TIMER="org.mortbay.cometd.client.Timer";
71      private final static String __JSON="org.mortbay.cometd.client.JSON";
72      private final static String __MSGPOOL="org.mortbay.cometd.MessagePool";
73      protected HttpClient _httpClient;
74  
75      protected MessagePool _msgPool;
76      private ArrayQueue<Message> _inQ = new ArrayQueue<Message>();  // queue of incoming messages 
77      private ArrayQueue<Message> _outQ = new ArrayQueue<Message>(); // queue of outgoing messages
78      protected Address _cometdAddress;
79      private Exchange _pull;
80      private Exchange _push;
81      private String _path = "/cometd";
82      private boolean _initialized = false;
83      private boolean _disconnecting = false;
84      private boolean _handshook = false;
85      private String _clientId;
86      private org.cometd.Listener _listener;
87      private List<RemoveListener> _rListeners;
88      private List<MessageListener> _mListeners;
89      private int _batch;
90      private boolean _formEncoded;
91      private Map<String, Cookie> _cookies = new ConcurrentHashMap<String, Cookie>();
92      private Advice _advice;
93      private Timer _timer;
94      private int _backoffInterval = 1000;
95      private int _backoffMaxRetries = 60; // equivalent to 60 seconds
96      private Buffer _scheme;
97      protected Extension[] _extensions;
98      protected JSON _jsonOut;
99      
100     /* ------------------------------------------------------------ */
101     public BayeuxClient(HttpClient client, String url)
102     {
103         this(client,url,null);
104     }
105     
106     /* ------------------------------------------------------------ */
107     public BayeuxClient(HttpClient client, String url, Timer timer)
108     {
109         HttpURI uri = new HttpURI(url);
110         _httpClient = client;
111         _cometdAddress = new Address(uri.getHost(),uri.getPort());
112         _path=uri.getPath();
113         _timer = timer;
114         _scheme = (HttpSchemes.HTTPS.equals(uri.getScheme()))?HttpSchemes.HTTPS_BUFFER:HttpSchemes.HTTP_BUFFER;
115     }
116     
117     /* ------------------------------------------------------------ */
118     public BayeuxClient(HttpClient client, Address address, String path, Timer timer)
119     {
120         _httpClient = client;
121         _cometdAddress = address;
122         _path = path;
123         _timer = timer;
124     }
125 
126     /* ------------------------------------------------------------ */
127     public BayeuxClient(HttpClient client, Address address, String uri)
128     {
129         this(client,address,uri,null);
130     }
131 
132     /* ------------------------------------------------------------ */
133     public void addExtension(Extension ext)
134     {
135         _extensions = (Extension[])LazyList.addToArray(_extensions,ext,Extension.class);
136     }
137 
138     /* ------------------------------------------------------------ */
139     Extension[] getExtensions()
140     {
141         return _extensions;
142     }
143     
144     /* ------------------------------------------------------------ */
145     /**
146      * If unable to connect/handshake etc, even if following the interval in the
147      * advice, wait for this interval and try again, up to a maximum of
148      * _backoffRetries
149      * 
150      * @param interval
151      */
152     public void setBackOffInterval(int interval)
153     {
154         _backoffInterval = interval;
155     }
156 
157     /* ------------------------------------------------------------ */
158     public int getBackoffInterval()
159     {
160         return _backoffInterval;
161     }
162 
163     /* ------------------------------------------------------------ */
164     public void setBackoffMaxRetries(int retries)
165     {
166         _backoffMaxRetries = retries;
167     }
168 
169     /* ------------------------------------------------------------ */
170     public int getBackoffMaxRetries()
171     {
172         return _backoffMaxRetries;
173     }
174 
175     /* ------------------------------------------------------------ */
176     /*
177      * (non-Javadoc) Returns the clientId
178      * 
179      * @see dojox.cometd.Client#getId()
180      */
181     public String getId()
182     {
183         return _clientId;
184     }
185 
186     /* ------------------------------------------------------------ */
187     protected void doStart() throws Exception
188     {
189         if (!_httpClient.isStarted())
190             throw new IllegalStateException("!HttpClient.isStarted()");
191             
192         synchronized (_httpClient)
193         {
194             if (_jsonOut == null)
195             {
196                 _jsonOut = (JSON)_httpClient.getAttribute(__JSON);
197                 if (_jsonOut==null)
198                 {
199                     _jsonOut = new JSON();
200                     _httpClient.setAttribute(__JSON,_jsonOut);
201                 }
202             }
203             
204             if (_timer == null)
205             {
206                 _timer = (Timer)_httpClient.getAttribute(__TIMER);
207                 if (_timer==null)
208                 {
209                     _timer = new Timer(__TIMER+"@"+hashCode(),true);
210                     _httpClient.setAttribute(__TIMER,_timer);
211                 }
212             }
213             
214             if (_msgPool == null)
215             {
216                 _msgPool = (MessagePool)_httpClient.getAttribute(__MSGPOOL);
217                 if (_msgPool==null)
218                 {
219                     _msgPool = new MessagePool();
220                     _httpClient.setAttribute(__MSGPOOL,_msgPool);
221                 }
222             }
223         }
224         _disconnecting=false;
225         _pull=null;
226         _push=null;
227         super.doStart();
228         synchronized (_outQ)
229         {
230             if (!_initialized && _pull == null)
231             {
232                 _pull = new Handshake();
233                 send((Exchange)_pull,false);
234             }
235         }
236     }
237 
238     /* ------------------------------------------------------------ */
239     protected void doStop() throws Exception
240     {
241         if (!_disconnecting)
242             disconnect();
243         super.doStop();
244     }
245 
246     /* ------------------------------------------------------------ */
247     public boolean isPolling()
248     {
249         synchronized (_outQ)
250         {
251             return isRunning() && (_pull != null);
252         }
253     }
254 
255     /* ------------------------------------------------------------ */
256     /**
257      * (non-Javadoc)
258      */
259     public void deliver(Client from, Message message)
260     {
261         if (!isRunning())
262             throw new IllegalStateException("Not running");
263 
264         synchronized (_inQ)
265         {
266             if (_mListeners == null)
267                 _inQ.add(message);
268             else
269             {
270                 for (MessageListener l : _mListeners)
271                     l.deliver(from,this,message);
272             }
273         }
274     }
275 
276     /* ------------------------------------------------------------ */
277     /*
278      * (non-Javadoc)
279      * 
280      * @see dojox.cometd.Client#deliver(dojox.cometd.Client, java.lang.String,
281      * java.lang.Object, java.lang.String)
282      */
283     public void deliver(Client from, String toChannel, Object data, String id)
284     {
285         if (!isRunning())
286             throw new IllegalStateException("Not running");
287 
288         MessageImpl message = _msgPool.newMessage();
289 
290         message.put(Bayeux.CHANNEL_FIELD,toChannel);
291         message.put(Bayeux.DATA_FIELD,data);
292         if (id != null)
293             message.put(Bayeux.ID_FIELD,id);
294 
295         synchronized (_inQ)
296         {
297             if (_mListeners == null)
298             {
299                 message.incRef();
300                 _inQ.add(message);
301             }
302             else
303             {
304                 for (MessageListener l : _mListeners)
305                     if (l instanceof MessageListener.Synchronous)
306                         l.deliver(from,this,message);
307             }
308         }
309         if (_mListeners !=null)
310             for (MessageListener l : _mListeners)
311                 if (!(l instanceof MessageListener.Synchronous))
312                 l.deliver(from,this,message);
313         message.decRef();
314     }
315 
316     /* ------------------------------------------------------------ */
317     /**
318      * @deprecated
319      */
320     public org.cometd.Listener getListener()
321     {
322         synchronized (_inQ)
323         {
324             return _listener;
325         }
326     }
327 
328     /* ------------------------------------------------------------ */
329     /*
330      * (non-Javadoc)
331      * 
332      * @see dojox.cometd.Client#hasMessages()
333      */
334     public boolean hasMessages()
335     {
336         synchronized (_inQ)
337         {
338             return _inQ.size() > 0;
339         }
340     }
341 
342     /* ------------------------------------------------------------ */
343     /*
344      * (non-Javadoc)
345      * 
346      * @see dojox.cometd.Client#isLocal()
347      */
348     public boolean isLocal()
349     {
350         return false;
351     }
352 
353     /* ------------------------------------------------------------ */
354     /*
355      * (non-Javadoc)
356      * 
357      * @see dojox.cometd.Client#subscribe(java.lang.String)
358      */
359     private void publish(MessageImpl msg)
360     {
361         msg.incRef();
362         synchronized (_outQ)
363         {
364             _outQ.add(msg);
365 
366             if (_batch == 0 && _initialized && _push == null)
367             {
368                 _push = new Publish();
369                 try
370                 {
371                     send(_push);
372                 }
373                 catch (Exception e)
374                 {
375                     metaPublishFail(e,((Publish)_push).getOutboundMessages());
376                 }
377             }
378         }
379     }
380 
381     /* ------------------------------------------------------------ */
382     /*
383      * (non-Javadoc)
384      * 
385      * @see dojox.cometd.Client#publish(java.lang.String, java.lang.Object,
386      * java.lang.String)
387      */
388     public void publish(String toChannel, Object data, String msgId)
389     {
390         if (!isRunning() || _disconnecting)
391             throw new IllegalStateException("Not running");
392 
393         MessageImpl msg = _msgPool.newMessage();
394         msg.put(Bayeux.CHANNEL_FIELD,toChannel);
395         msg.put(Bayeux.DATA_FIELD,data);
396         if (msgId != null)
397             msg.put(Bayeux.ID_FIELD,msgId);
398         publish(msg);
399         msg.decRef();
400     }
401 
402     /* ------------------------------------------------------------ */
403     /*
404      * (non-Javadoc)
405      * 
406      * @see dojox.cometd.Client#subscribe(java.lang.String)
407      */
408     public void subscribe(String toChannel)
409     {
410         if (!isRunning() || _disconnecting)
411             throw new IllegalStateException("Not running");
412 
413         MessageImpl msg = _msgPool.newMessage();
414         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_SUBSCRIBE);
415         msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel);
416         publish(msg);
417         msg.decRef();
418     }
419 
420     /* ------------------------------------------------------------ */
421     /*
422      * (non-Javadoc)
423      * 
424      * @see dojox.cometd.Client#unsubscribe(java.lang.String)
425      */
426     public void unsubscribe(String toChannel)
427     {
428         if (!isRunning() || _disconnecting)
429             throw new IllegalStateException("Not running");
430 
431         MessageImpl msg = _msgPool.newMessage();
432         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_UNSUBSCRIBE);
433         msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel);
434         publish(msg);
435         msg.decRef();
436     }
437 
438     /* ------------------------------------------------------------ */
439     /**
440      * Disconnect this client.
441      * @deprecated use {@link #disconnect()}
442      */
443     public void remove()
444     {
445         disconnect();
446     }
447     
448     /* ------------------------------------------------------------ */
449     /**
450      * Disconnect this client.
451      */
452     public void disconnect()
453     {
454         if (!isRunning() || _disconnecting)
455             throw new IllegalStateException("Not running");
456 
457         MessageImpl msg = _msgPool.newMessage();
458         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_DISCONNECT);
459 
460         synchronized (_outQ)
461         {
462             _outQ.add(msg);
463             _disconnecting = true;
464             if (_batch == 0 && _initialized && _push == null)
465             {
466                 _push = new Publish();
467                 try
468                 {
469                     send(_push);
470                 }
471                 catch (IOException e)
472                 {
473                     Log.warn(e.toString());
474                     Log.debug(e);
475                     send(_push,true);
476                 }
477             }
478             _initialized = false;
479         }
480     }
481 
482     /* ------------------------------------------------------------ */
483     /**
484      * @deprecated
485      */
486     public void setListener(org.cometd.Listener listener)
487     {
488         synchronized (_inQ)
489         {
490             if (_listener != null)
491                 removeListener(_listener);
492             _listener = listener;
493             if (_listener != null)
494                 addListener(_listener);
495         }
496     }
497 
498     /* ------------------------------------------------------------ */
499     /*
500      * (non-Javadoc) Removes all available messages from the inbound queue. If a
501      * listener is set then messages are not queued.
502      * 
503      * @see dojox.cometd.Client#takeMessages()
504      */
505     public List<Message> takeMessages()
506     {
507         final LinkedList<Message> list;
508         synchronized (_inQ)
509         {
510             list = new LinkedList<Message>(_inQ);
511             _inQ.clear();
512         }
513         for (Message m : list)
514             if (m instanceof MessageImpl)
515                 ((MessageImpl)m).decRef();
516         return list;
517     }
518 
519     /* ------------------------------------------------------------ */
520     /*
521      * (non-Javadoc)
522      * 
523      * @see dojox.cometd.Client#endBatch()
524      */
525     public void endBatch()
526     {
527         synchronized (_outQ)
528         {
529             if (--_batch <= 0)
530             {
531                 _batch = 0;
532                 if ((_initialized || _disconnecting) && _push == null && _outQ.size() > 0)
533                 {
534                     _push = new Publish();
535                     try
536                     {
537                         send(_push);
538                     }
539                     catch (IOException e)
540                     {
541                         metaPublishFail(e,((Publish)_push).getOutboundMessages());
542                     }
543                 }
544             }
545         }
546     }
547 
548     /* ------------------------------------------------------------ */
549     /*
550      * (non-Javadoc)
551      * 
552      * @see dojox.cometd.Client#startBatch()
553      */
554     public void startBatch()
555     {
556         if (!isRunning())
557             throw new IllegalStateException("Not running");
558 
559         synchronized (_outQ)
560         {
561             _batch++;
562         }
563     }
564 
565     /* ------------------------------------------------------------ */
566     /**
567      * Customize an Exchange. Called when an exchange is about to be sent to
568      * allow Cookies and Credentials to be customized. Default implementation
569      * sets any cookies
570      */
571     protected void customize(HttpExchange exchange)
572     {
573         StringBuilder buf = null;
574         for (Cookie cookie : _cookies.values())
575         {
576             if (buf == null)
577                 buf = new StringBuilder();
578             else
579                 buf.append("; ");
580             buf.append(cookie.getName()); // TODO quotes
581             buf.append("=");
582             buf.append(cookie.getValue()); // TODO quotes
583         }
584         if (buf != null)
585             exchange.setRequestHeader(HttpHeaders.COOKIE,buf.toString());
586       
587         if (_scheme!=null)
588             exchange.setScheme(_scheme);
589     }
590 
591     /* ------------------------------------------------------------ */
592     public void setCookie(Cookie cookie)
593     {
594         _cookies.put(cookie.getName(),cookie);
595     }
596 
597     /* ------------------------------------------------------------ */
598     /* ------------------------------------------------------------ */
599     /* ------------------------------------------------------------ */
600     /**
601      * The base class for all bayeux exchanges.
602      */
603     protected class Exchange extends ContentExchange
604     {
605         Message[] _responses;
606         int _connectFailures;
607         int _backoffRetries = 0;
608         String _json;
609 
610         /* ------------------------------------------------------------ */
611         Exchange(String info)
612         {
613             setMethod("POST");
614             setScheme(HttpSchemes.HTTP_BUFFER);
615             setAddress(_cometdAddress);
616             setURI(_path + "/" + info);
617             setRequestContentType(_formEncoded?"application/x-www-form-urlencoded;charset=utf-8":"text/json;charset=utf-8");
618         }
619 
620         /* ------------------------------------------------------------ */
621         public int getBackoffRetries()
622         {
623             return _backoffRetries;
624         }
625 
626         /* ------------------------------------------------------------ */
627         public void incBackoffRetries()
628         {
629             ++_backoffRetries;
630         }
631 
632         /* ------------------------------------------------------------ */
633         protected void setMessage(String message)
634         {
635             message=extendOut(message);
636             setJson(message);
637         }
638 
639         /* ------------------------------------------------------------ */
640         protected void setJson(String json)
641         {
642             try
643             {
644                 _json = json;
645 
646                 if (_formEncoded)
647                     setRequestContent(new ByteArrayBuffer("message=" + URLEncoder.encode(_json,"utf-8")));
648                 else
649                     setRequestContent(new ByteArrayBuffer(_json,"utf-8"));
650             }
651             catch (Exception e)
652             {
653                 Log.ignore(e);
654                 setRequestContent(new ByteArrayBuffer(_json));
655             }
656         }
657 
658         /* ------------------------------------------------------------ */
659         protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
660         {
661             super.onResponseStatus(version,status,reason);
662         }
663 
664         /* ------------------------------------------------------------ */
665         protected void onResponseHeader(Buffer name, Buffer value) throws IOException
666         {
667             super.onResponseHeader(name,value);
668             if (!isRunning())
669                 return;
670 
671             if (HttpHeaders.CACHE.getOrdinal(name) == HttpHeaders.SET_COOKIE_ORDINAL)
672             {
673                 String cname = null;
674                 String cvalue = null;
675 
676                 QuotedStringTokenizer tok = new QuotedStringTokenizer(value.toString(),"=;",false,false);
677                 tok.setSingle(false);
678 
679                 if (tok.hasMoreElements())
680                     cname = tok.nextToken();
681                 if (tok.hasMoreElements())
682                     cvalue = tok.nextToken();
683 
684                 Cookie cookie = new Cookie(cname,cvalue);
685 
686                 while (tok.hasMoreTokens())
687                 {
688                     String token = tok.nextToken();
689                     if ("Version".equalsIgnoreCase(token))
690                         cookie.setVersion(Integer.parseInt(tok.nextToken()));
691                     else if ("Comment".equalsIgnoreCase(token))
692                         cookie.setComment(tok.nextToken());
693                     else if ("Path".equalsIgnoreCase(token))
694                         cookie.setPath(tok.nextToken());
695                     else if ("Domain".equalsIgnoreCase(token))
696                         cookie.setDomain(tok.nextToken());
697                     else if ("Expires".equalsIgnoreCase(token))
698                     {
699                         tok.nextToken();
700                         // TODO
701                     }
702                     else if ("Max-Age".equalsIgnoreCase(token))
703                     {
704                         tok.nextToken();
705                         // TODO
706                     }
707                     else if ("Secure".equalsIgnoreCase(token))
708                         cookie.setSecure(true);
709                 }
710 
711                 BayeuxClient.this.setCookie(cookie);
712             }
713         }
714 
715         /* ------------------------------------------------------------ */
716         protected void onResponseComplete() throws IOException
717         {
718             if (!isRunning())
719                 return;
720 
721             super.onResponseComplete();
722 
723             if (getResponseStatus() == 200)
724             {
725                 String content = getResponseContent();
726                 // TODO
727                 if (content == null || content.length() == 0)
728                     throw new IllegalStateException();
729                 _responses = _msgPool.parse(content);
730                 
731                 if (_responses!=null)
732                     for (int i=0;i<_responses.length;i++)
733                         extendIn(_responses[i]);
734             }
735         }
736 
737         /* ------------------------------------------------------------ */
738         protected void resend(boolean backoff)
739         {
740             if (!isRunning())
741                 return;
742 
743             final boolean disconnecting;
744             synchronized (_outQ)
745             {
746                 disconnecting=_disconnecting;
747             }
748             if (disconnecting)
749             {
750                 try{stop();}catch(Exception e){Log.ignore(e);}
751                 return;
752             }
753             
754             setJson(_json);
755             if (!send(this,backoff))
756                 Log.warn("Retries exhausted"); // giving up
757         }
758         
759         /* ------------------------------------------------------------ */
760         protected void recycle()
761         {
762             if (_responses!=null)
763                 for (Message msg:_responses)
764                     if (msg instanceof MessageImpl)
765                         ((MessageImpl)msg).decRef();
766             _responses=null;
767         }
768     }
769 
770     /* ------------------------------------------------------------ */
771     /**
772      * The Bayeux handshake exchange. Negotiates a client Id and initializes the
773      * protocol.
774      * 
775      */
776     protected class Handshake extends Exchange
777     {
778         public final static String __HANDSHAKE = "[{" + "\"channel\":\"/meta/handshake\"," + "\"version\":\"0.9\"," + "\"minimumVersion\":\"0.9\"" + "}]";
779 
780         Handshake()
781         {
782             super("handshake");
783             setMessage(__HANDSHAKE);
784         }
785 
786         /* ------------------------------------------------------------ */
787         /*
788          * (non-Javadoc)
789          * 
790          * @see
791          * org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete()
792          */
793         protected void onResponseComplete() throws IOException
794         {
795             super.onResponseComplete();
796 
797             if (!isRunning())
798                 return;    
799             
800             if (_disconnecting)
801             {
802                 Message error=_msgPool.newMessage();
803                 error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
804                 error.put("failure","expired");
805                 metaHandshake(false,false,error);
806                 try{stop();}catch(Exception e){Log.ignore(e);}
807                 return;
808             }
809 
810             if (getResponseStatus() == 200 && _responses != null && _responses.length > 0)
811             {
812                 MessageImpl response = (MessageImpl)_responses[0];
813                 boolean successful = response.isSuccessful();
814 
815                 // Get advice if there is any
816                 Map adviceField = (Map)response.get(Bayeux.ADVICE_FIELD);
817                 if (adviceField != null)
818                     _advice = new Advice(adviceField);
819 
820                 if (successful)
821                 {
822                     _handshook = true;
823                     if (Log.isDebugEnabled())
824                         Log.debug("Successful handshake, sending connect");
825                     _clientId = (String)response.get(Bayeux.CLIENT_FIELD);
826 
827                     metaHandshake(true,_handshook,response);
828                     _pull = new Connect();
829                     send(_pull,false);
830                 }
831                 else
832                 {
833                     metaHandshake(false,false,response);
834                     _handshook = false;
835                     if (_advice != null && _advice.isReconnectNone())
836                         throw new IOException("Handshake failed with advice reconnect=none :" + _responses[0]);
837                     else if (_advice != null && _advice.isReconnectHandshake())
838                     {
839                         _pull = new Handshake();
840                         if (!send(_pull,true))
841                             throw new IOException("Handshake, retries exhausted");
842                     }
843                     else
844                     // assume retry = reconnect?
845                     {
846                         _pull = new Connect();
847                         if (!send(_pull,true))
848                             throw new IOException("Connect after handshake, retries exhausted");
849                     }
850                 }
851             }
852             else
853             {
854                 Message error=_msgPool.newMessage();
855                 error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
856                 error.put("status",new Integer(getResponseStatus()));
857                 error.put("content",getResponseContent());
858                 
859                 metaHandshake(false,false,error);
860                 resend(true);
861             }
862             
863             recycle();
864         }
865 
866         /* ------------------------------------------------------------ */
867         protected void onExpire()
868         {
869             // super.onExpire();
870             Message error=_msgPool.newMessage();
871             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
872             error.put("failure","expired");
873             metaHandshake(false,false,error);
874             resend(true);
875         }
876 
877         /* ------------------------------------------------------------ */
878         protected void onConnectionFailed(Throwable ex)
879         {
880             // super.onConnectionFailed(ex);
881             Message error=_msgPool.newMessage();
882             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
883             error.put("failure",ex.toString());
884             error.put("exception",ex);
885             ex.printStackTrace();
886             metaHandshake(false,false,error);
887             resend(true);
888         }
889 
890         /* ------------------------------------------------------------ */
891         protected void onException(Throwable ex)
892         {
893             // super.onException(ex);
894             Message error=_msgPool.newMessage();
895             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
896             error.put("failure",ex.toString());
897             error.put("exception",ex);
898             metaHandshake(false,false,error);
899             resend(true);
900         }
901     }
902 
903     /* ------------------------------------------------------------ */
904     /**
905      * The Bayeux Connect exchange. Connect exchanges implement the long poll
906      * for Bayeux.
907      */
908     protected class Connect extends Exchange
909     {
910         String _connectString;
911 
912         Connect()
913         {
914             super("connect");
915             _connectString = "{" + "\"channel\":\"/meta/connect\"," + "\"clientId\":\"" + _clientId + "\"," + "\"connectionType\":\"long-polling\"" + "}";
916             setMessage(_connectString);
917         }
918 
919         protected void onResponseComplete() throws IOException
920         {
921             super.onResponseComplete();
922             if (!isRunning())
923                 return;
924 
925             if (getResponseStatus() == 200 && _responses != null && _responses.length > 0)
926             {
927                 try
928                 {
929                     startBatch();
930 
931                     for (int i = 0; i < _responses.length; i++)
932                     {
933                         Message msg = _responses[i];
934 
935                         // get advice if there is any
936                         Map adviceField = (Map)msg.get(Bayeux.ADVICE_FIELD);
937                         if (adviceField != null)
938                             _advice = new Advice(adviceField);
939 
940                         if (Bayeux.META_CONNECT.equals(msg.get(Bayeux.CHANNEL_FIELD)))
941                         {
942                             Boolean successful = (Boolean)msg.get(Bayeux.SUCCESSFUL_FIELD);
943                             if (successful != null && successful.booleanValue())
944                             {                               
945                                 metaConnect(true,msg);
946 
947                                 if (!isRunning())
948                                     break;
949                                 
950                                 synchronized (_outQ)
951                                 {
952                                     if (_disconnecting)
953                                         continue;
954                                     
955                                     if (!isInitialized())
956                                     {
957                                         setInitialized(true);
958                                         {
959                                             if (_outQ.size() > 0)
960                                             {
961                                                 _push = new Publish();
962                                                 send(_push);
963                                             }
964                                         }
965                                     }
966 
967                                 }
968                                 // send a Connect (ie longpoll) possibly with
969                                 // delay according to interval advice                                
970                                 _pull = new Connect();
971                                 send(_pull,false);
972                             }
973                             else
974                             {
975                                 // received a failure to our connect message,
976                                 // check the advice to see what to do:
977                                 // reconnect: none = hard error
978                                 // reconnect: handshake = send a handshake
979                                 // message
980                                 // reconnect: retry = send another connect,
981                                 // possibly using interval
982 
983                                 setInitialized(false);
984                                 metaConnect(false,msg);
985                                 
986                                 synchronized(_outQ)
987                                 {
988                                     if (!isRunning()||_disconnecting)
989                                         break;
990                                 }
991                                 
992                                 if (_advice != null && _advice.isReconnectNone())
993                                     throw new IOException("Connect failed, advice reconnect=none");
994                                 else if (_advice != null && _advice.isReconnectHandshake())
995                                 {
996                                     if (Log.isDebugEnabled())
997                                         Log.debug("connect received success=false, advice is to rehandshake");
998                                     _pull = new Handshake();
999                                     send(_pull,true);
1000                                 }
1001                                 else
1002                                 {
1003                                     // assume retry = reconnect
1004                                     if (Log.isDebugEnabled())
1005                                         Log.debug("Assuming retry=reconnect");
1006                                     resend(true);
1007                                 }
1008                             }
1009                         }
1010                         deliver(null,msg);
1011                     }
1012                 }
1013                 finally
1014                 {
1015                     endBatch();
1016                 }
1017             }
1018             else
1019             {
1020                 Message error=_msgPool.newMessage();
1021                 error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1022                 error.put("status",getResponseStatus());
1023                 error.put("content",getResponseContent());
1024                 metaConnect(false,error);
1025                 resend(true);
1026             }
1027 
1028             recycle();
1029         }
1030 
1031         /* ------------------------------------------------------------ */
1032         protected void onExpire()
1033         {
1034             // super.onExpire();
1035             setInitialized(false);
1036             Message error=_msgPool.newMessage();
1037             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1038             error.put("failure","expired");
1039             metaConnect(false,error);
1040             resend(true);
1041         }
1042 
1043         /* ------------------------------------------------------------ */
1044         protected void onConnectionFailed(Throwable ex)
1045         {
1046             // super.onConnectionFailed(ex);
1047             setInitialized(false);
1048             Message error=_msgPool.newMessage();
1049             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1050             error.put("failure",ex.toString());
1051             error.put("exception",ex);
1052             metaConnect(false,error);
1053             resend(true);
1054         }
1055 
1056         /* ------------------------------------------------------------ */
1057         protected void onException(Throwable ex)
1058         {
1059             // super.onException(ex);
1060             setInitialized(false);
1061             Message error=_msgPool.newMessage();
1062             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1063             error.put("failure",ex.toString());
1064             error.put("exception",ex);
1065             metaConnect(false,error);
1066             resend(true);
1067         }
1068     }
1069 
1070     /* ------------------------------------------------------------ */
1071     /**
1072      * Publish message exchange. Sends messages to bayeux server and handles any
1073      * messages received as a result.
1074      */
1075     protected class Publish extends Exchange
1076     {
1077         Publish()
1078         {
1079             super("publish");
1080             
1081             StringBuffer json = new StringBuffer(256);
1082             synchronized (json)
1083             {
1084                 synchronized (_outQ)
1085                 {
1086                     int s=_outQ.size();
1087                     if (s == 0)
1088                         return;
1089 
1090                     for (int i=0;i<s;i++)
1091                     {
1092                         Message message = _outQ.getUnsafe(i);
1093                         message.put(Bayeux.CLIENT_FIELD,_clientId);
1094                         extendOut(message);
1095 
1096                         json.append(i==0?'[':',');
1097                         _jsonOut.append(json,message);
1098 
1099                         if (message instanceof MessageImpl)
1100                             ((MessageImpl)message).decRef();
1101                     }
1102                     json.append(']');
1103                     _outQ.clear();
1104                     setJson(json.toString());
1105                 }
1106             }
1107         }
1108 
1109         protected Message[] getOutboundMessages()
1110         {
1111             try
1112             {
1113                 return _msgPool.parse(_json);
1114             }
1115             catch (IOException e)
1116             {
1117                 Log.warn("Error converting outbound messages");
1118                 if (Log.isDebugEnabled())
1119                     Log.debug(e);
1120                 return null;
1121             }
1122         }
1123 
1124         /* ------------------------------------------------------------ */
1125         /*
1126          * (non-Javadoc)
1127          * 
1128          * @see
1129          * org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete()
1130          */
1131         protected void onResponseComplete() throws IOException
1132         {
1133             super.onResponseComplete();
1134             try
1135             {
1136                 synchronized (_outQ)
1137                 {
1138                     startBatch();
1139                     _push = null;
1140                 }
1141 
1142                 if (getResponseStatus() == 200 && _responses != null && _responses.length > 0)
1143                 {
1144                     for (int i = 0; i < _responses.length; i++)
1145                     {
1146                         MessageImpl msg = (MessageImpl)_responses[i];
1147                             
1148                         deliver(null,msg);
1149                         if (Bayeux.META_DISCONNECT.equals(msg.getChannel())&&msg.isSuccessful())
1150                         {
1151                             if (isStarted())
1152                             {
1153                                 try{stop();}catch(Exception e){Log.ignore(e);}
1154                             }
1155                             break;
1156                         }
1157                     }
1158                 }
1159                 else
1160                 {
1161                     Log.warn("Publish, error=" + getResponseStatus());
1162                 }
1163             }
1164             finally
1165             {
1166                 endBatch();
1167             }
1168             recycle();
1169         }
1170 
1171         /* ------------------------------------------------------------ */
1172         protected void onExpire()
1173         {
1174             super.onExpire();
1175             metaPublishFail(null,this.getOutboundMessages());
1176             if (_disconnecting)
1177             {
1178                 try{stop();}catch(Exception e){Log.ignore(e);}
1179             }
1180         }
1181 
1182         /* ------------------------------------------------------------ */
1183         protected void onConnectionFailed(Throwable ex)
1184         {
1185             super.onConnectionFailed(ex);
1186             metaPublishFail(ex,this.getOutboundMessages());
1187             if (_disconnecting)
1188             {
1189                 try{stop();}catch(Exception e){Log.ignore(e);}
1190             }
1191         }
1192 
1193         /* ------------------------------------------------------------ */
1194         protected void onException(Throwable ex)
1195         {
1196             super.onException(ex);
1197             metaPublishFail(ex,this.getOutboundMessages());
1198             if (_disconnecting)
1199             {
1200                 try{stop();}catch(Exception e){Log.ignore(e);}
1201             }
1202         }
1203     }
1204 
1205     /* ------------------------------------------------------------ */
1206     public void addListener(ClientListener listener)
1207     {
1208         synchronized (_inQ)
1209         {
1210             boolean added=false;
1211             if (listener instanceof MessageListener)
1212             {
1213                 added=true;
1214                 if (_mListeners == null)
1215                     _mListeners = new ArrayList<MessageListener>();
1216                 _mListeners.add((MessageListener)listener);
1217             }
1218             if (listener instanceof RemoveListener)
1219             {
1220                 added=true;
1221                 if (_rListeners == null)
1222                     _rListeners = new ArrayList<RemoveListener>();
1223                 _rListeners.add((RemoveListener)listener);
1224             }
1225             
1226             if (!added)
1227                 throw new IllegalArgumentException();
1228         }
1229     }
1230 
1231     /* ------------------------------------------------------------ */
1232     public void removeListener(ClientListener listener)
1233     {
1234         synchronized (_inQ)
1235         {
1236             if (listener instanceof MessageListener)
1237             {
1238                 if (_mListeners != null)
1239                     _mListeners.remove((MessageListener)listener);
1240             }
1241             if (listener instanceof RemoveListener)
1242             {
1243                 if (_rListeners != null)
1244                     _rListeners.remove((RemoveListener)listener);
1245             }
1246         }
1247     }
1248 
1249     /* ------------------------------------------------------------ */
1250     public int getMaxQueue()
1251     {
1252         return -1;
1253     }
1254 
1255     /* ------------------------------------------------------------ */
1256     public Queue<Message> getQueue()
1257     {
1258         return _inQ;
1259     }
1260 
1261     /* ------------------------------------------------------------ */
1262     public void setMaxQueue(int max)
1263     {
1264         if (max != -1)
1265             throw new UnsupportedOperationException();
1266     }
1267 
1268     /* ------------------------------------------------------------ */
1269     /**
1270      * Send the exchange, possibly using a backoff.
1271      * 
1272      * @param exchange
1273      * @param backoff
1274      *            if true, use backoff algorithm to send
1275      * @return
1276      */
1277     protected boolean send(final Exchange exchange, final boolean backoff)
1278     {
1279         long interval = (_advice != null?_advice.getInterval():0);
1280 
1281         if (backoff)
1282         {
1283             int retries = exchange.getBackoffRetries();
1284             if (Log.isDebugEnabled())
1285                 Log.debug("Send with backoff, retries=" + retries + " for " + exchange);
1286             if (retries >= _backoffMaxRetries)
1287                 return false;
1288 
1289             exchange.incBackoffRetries();
1290             interval += (retries * _backoffInterval);
1291         }
1292 
1293         if (interval > 0)
1294         {
1295             TimerTask task = new TimerTask()
1296             {
1297                 public void run()
1298                 {
1299                     try
1300                     {
1301                         send(exchange);
1302                     }
1303                     catch (IOException e)
1304                     {
1305                         Log.warn("Delayed send, retry: "+e);
1306                         Log.debug(e);
1307                         send(exchange,true);
1308                     }
1309                 }
1310             };
1311             if (Log.isDebugEnabled())
1312                 Log.debug("Delay " + interval + " send of " + exchange);
1313             _timer.schedule(task,interval);
1314         }
1315         else
1316         {
1317             try
1318             {
1319                 send(exchange);
1320             }
1321             catch (IOException e)
1322             {
1323                 Log.warn("Send, retry on fail: "+e);
1324                 Log.debug(e);
1325                 return send(exchange,true);
1326             }
1327         }
1328         return true;
1329 
1330     }
1331 
1332     /* ------------------------------------------------------------ */
1333     /**
1334      * Send the exchange.
1335      * 
1336      * @param exchange
1337      * @throws IOException
1338      */
1339     protected void send(HttpExchange exchange) throws IOException
1340     {
1341         exchange.reset(); // ensure at start state
1342         customize(exchange);
1343         if (Log.isDebugEnabled())
1344             Log.debug("Send: using any connection=" + exchange);
1345         _httpClient.send(exchange); // use any connection
1346     }
1347 
1348     /* ------------------------------------------------------------ */
1349     /**
1350      * False when we have received a success=false message in response to a
1351      * Connect, or we have had an exception when sending or receiving a Connect.
1352      * 
1353      * True when handshake and then connect has happened.
1354      * 
1355      * @param b
1356      */
1357     protected void setInitialized(boolean b)
1358     {
1359         synchronized (_outQ)
1360         {
1361             _initialized = b;
1362         }
1363     }
1364 
1365     /* ------------------------------------------------------------ */
1366     protected boolean isInitialized()
1367     {
1368         return _initialized;
1369     }
1370 
1371     /* ------------------------------------------------------------ */
1372     /**
1373      * Called with the results of a /meta/connect message
1374      * @param success connect was returned with this status
1375      */
1376     protected void metaConnect(boolean success, Message message)
1377     {
1378         if (!success)
1379             Log.warn(this.toString()+" "+message.toString());
1380     }
1381 
1382     /* ------------------------------------------------------------ */
1383     /**
1384      * Called with the results of a /meta/handshake message
1385      * @param success connect was returned with this status
1386      * @param reestablish the client was previously connected.
1387      */
1388     protected void metaHandshake(boolean success, boolean reestablish, Message message)
1389     {
1390         if (!success)
1391             Log.warn(this.toString()+" "+message.toString());
1392     }
1393 
1394     /* ------------------------------------------------------------ */
1395     /**
1396      * Called with the results of a failed publish
1397      */
1398     protected void metaPublishFail(Throwable e, Message[] messages)
1399     {
1400         Log.warn(this.toString()+": "+e);
1401         Log.debug(e);
1402     }
1403 
1404     /* ------------------------------------------------------------ */
1405     /** Called to extend outbound string messages.
1406      * Some messages are sent as preformatted JSON strings (eg handshake
1407      * and connect messages).  This extendOut method is a variation of the
1408      * {@link #extendOut(Message)} method to efficiently cater for these
1409      * preformatted strings.
1410      * <p>
1411      * This method calls the {@link Extension}s added by {@link #addExtension(Extension)}
1412      * 
1413      * @param msg
1414      * @return the extended message
1415      */
1416     protected String extendOut(String msg)
1417     {
1418         if (_extensions==null)
1419             return msg;
1420         
1421         try
1422         {
1423             Message[] messages = _msgPool.parse(msg);
1424             for (int i=0; i<messages.length; i++)
1425                 extendOut(messages[i]);
1426             if (messages.length==1 && msg.charAt(0)=='{')
1427                 return _msgPool.getMsgJSON().toJSON(messages[0]);
1428             return _msgPool.getMsgJSON().toJSON(messages);
1429         }
1430         catch(IOException e)
1431         {
1432             Log.warn(e);
1433             return msg;
1434         }
1435     }
1436 
1437     /* ------------------------------------------------------------ */
1438     /** Called to extend outbound messages
1439      * <p>
1440      * This method calls the {@link Extension}s added by {@link #addExtension(Extension)}
1441      * 
1442      */
1443     protected void extendOut(Message message)
1444     {
1445         if (_extensions!=null)
1446         {
1447             Message m = message;
1448             if (m.getChannel().startsWith(Bayeux.META_SLASH))
1449                 for (int i=0;m!=null && i<_extensions.length;i++)
1450                     m=_extensions[i].sendMeta(this,m);
1451             else
1452                 for (int i=0;m!=null && i<_extensions.length;i++)
1453                     m=_extensions[i].send(this,m);
1454                 
1455             if (message!=m)
1456             {
1457                 message.clear();
1458                 if (m!=null)
1459                     for (Map.Entry<String,Object> entry:m.entrySet())
1460                         message.put(entry.getKey(),entry.getValue());
1461             }
1462         }
1463     }
1464 
1465     /* ------------------------------------------------------------ */
1466     /** Called to extend inbound messages
1467      * <p>
1468      * This method calls the {@link Extension}s added by {@link #addExtension(Extension)}
1469      * 
1470      */
1471     protected void extendIn(Message message)
1472     {
1473         if (_extensions!=null)
1474         {
1475             Message m = message;
1476             if (m.getChannel().startsWith(Bayeux.META_SLASH))
1477                 for (int i=_extensions.length;m!=null && i-->0;)
1478                     m=_extensions[i].rcvMeta(this,m);
1479             else
1480                 for (int i=_extensions.length;m!=null && i-->0;)
1481                     m=_extensions[i].rcv(this,m);
1482                 
1483             if (message!=m)
1484             {
1485                 message.clear();
1486                 if (m!=null)
1487                     for (Map.Entry<String,Object> entry:m.entrySet())
1488                         message.put(entry.getKey(),entry.getValue());
1489             }
1490         }
1491     }
1492 
1493     
1494 }