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                     l.deliver(from,this,message);
306             }
307         }
308         message.decRef();
309     }
310 
311     /* ------------------------------------------------------------ */
312     /**
313      * @deprecated
314      */
315     public org.cometd.Listener getListener()
316     {
317         synchronized (_inQ)
318         {
319             return _listener;
320         }
321     }
322 
323     /* ------------------------------------------------------------ */
324     /*
325      * (non-Javadoc)
326      * 
327      * @see dojox.cometd.Client#hasMessages()
328      */
329     public boolean hasMessages()
330     {
331         synchronized (_inQ)
332         {
333             return _inQ.size() > 0;
334         }
335     }
336 
337     /* ------------------------------------------------------------ */
338     /*
339      * (non-Javadoc)
340      * 
341      * @see dojox.cometd.Client#isLocal()
342      */
343     public boolean isLocal()
344     {
345         return false;
346     }
347 
348     /* ------------------------------------------------------------ */
349     /*
350      * (non-Javadoc)
351      * 
352      * @see dojox.cometd.Client#subscribe(java.lang.String)
353      */
354     private void publish(MessageImpl msg)
355     {
356         msg.incRef();
357         synchronized (_outQ)
358         {
359             _outQ.add(msg);
360 
361             if (_batch == 0 && _initialized && _push == null)
362             {
363                 _push = new Publish();
364                 try
365                 {
366                     send(_push);
367                 }
368                 catch (Exception e)
369                 {
370                     metaPublishFail(e,((Publish)_push).getOutboundMessages());
371                 }
372             }
373         }
374     }
375 
376     /* ------------------------------------------------------------ */
377     /*
378      * (non-Javadoc)
379      * 
380      * @see dojox.cometd.Client#publish(java.lang.String, java.lang.Object,
381      * java.lang.String)
382      */
383     public void publish(String toChannel, Object data, String msgId)
384     {
385         if (!isRunning() || _disconnecting)
386             throw new IllegalStateException("Not running");
387 
388         MessageImpl msg = _msgPool.newMessage();
389         msg.put(Bayeux.CHANNEL_FIELD,toChannel);
390         msg.put(Bayeux.DATA_FIELD,data);
391         if (msgId != null)
392             msg.put(Bayeux.ID_FIELD,msgId);
393         publish(msg);
394         msg.decRef();
395     }
396 
397     /* ------------------------------------------------------------ */
398     /*
399      * (non-Javadoc)
400      * 
401      * @see dojox.cometd.Client#subscribe(java.lang.String)
402      */
403     public void subscribe(String toChannel)
404     {
405         if (!isRunning() || _disconnecting)
406             throw new IllegalStateException("Not running");
407 
408         MessageImpl msg = _msgPool.newMessage();
409         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_SUBSCRIBE);
410         msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel);
411         publish(msg);
412         msg.decRef();
413     }
414 
415     /* ------------------------------------------------------------ */
416     /*
417      * (non-Javadoc)
418      * 
419      * @see dojox.cometd.Client#unsubscribe(java.lang.String)
420      */
421     public void unsubscribe(String toChannel)
422     {
423         if (!isRunning() || _disconnecting)
424             throw new IllegalStateException("Not running");
425 
426         MessageImpl msg = _msgPool.newMessage();
427         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_UNSUBSCRIBE);
428         msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel);
429         publish(msg);
430         msg.decRef();
431     }
432 
433     /* ------------------------------------------------------------ */
434     /**
435      * Disconnect this client.
436      * @deprecated use {@link #disconnect()}
437      */
438     public void remove()
439     {
440         disconnect();
441     }
442     
443     /* ------------------------------------------------------------ */
444     /**
445      * Disconnect this client.
446      */
447     public void disconnect()
448     {
449         if (!isRunning() || _disconnecting)
450             throw new IllegalStateException("Not running");
451 
452         MessageImpl msg = _msgPool.newMessage();
453         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_DISCONNECT);
454 
455         synchronized (_outQ)
456         {
457             _outQ.add(msg);
458             _disconnecting = true;
459             if (_batch == 0 && _initialized && _push == null)
460             {
461                 _push = new Publish();
462                 try
463                 {
464                     send(_push);
465                 }
466                 catch (IOException e)
467                 {
468                     Log.warn(e.toString());
469                     Log.debug(e);
470                     send(_push,true);
471                 }
472             }
473             _initialized = false;
474         }
475     }
476 
477     /* ------------------------------------------------------------ */
478     /**
479      * @deprecated
480      */
481     public void setListener(org.cometd.Listener listener)
482     {
483         synchronized (_inQ)
484         {
485             if (_listener != null)
486                 removeListener(_listener);
487             _listener = listener;
488             if (_listener != null)
489                 addListener(_listener);
490         }
491     }
492 
493     /* ------------------------------------------------------------ */
494     /*
495      * (non-Javadoc) Removes all available messages from the inbound queue. If a
496      * listener is set then messages are not queued.
497      * 
498      * @see dojox.cometd.Client#takeMessages()
499      */
500     public List<Message> takeMessages()
501     {
502         final LinkedList<Message> list;
503         synchronized (_inQ)
504         {
505             list = new LinkedList<Message>(_inQ);
506             _inQ.clear();
507         }
508         for (Message m : list)
509             if (m instanceof MessageImpl)
510                 ((MessageImpl)m).decRef();
511         return list;
512     }
513 
514     /* ------------------------------------------------------------ */
515     /*
516      * (non-Javadoc)
517      * 
518      * @see dojox.cometd.Client#endBatch()
519      */
520     public void endBatch()
521     {
522         synchronized (_outQ)
523         {
524             if (--_batch <= 0)
525             {
526                 _batch = 0;
527                 if ((_initialized || _disconnecting) && _push == null && _outQ.size() > 0)
528                 {
529                     _push = new Publish();
530                     try
531                     {
532                         send(_push);
533                     }
534                     catch (IOException e)
535                     {
536                         metaPublishFail(e,((Publish)_push).getOutboundMessages());
537                     }
538                 }
539             }
540         }
541     }
542 
543     /* ------------------------------------------------------------ */
544     /*
545      * (non-Javadoc)
546      * 
547      * @see dojox.cometd.Client#startBatch()
548      */
549     public void startBatch()
550     {
551         if (!isRunning())
552             throw new IllegalStateException("Not running");
553 
554         synchronized (_outQ)
555         {
556             _batch++;
557         }
558     }
559 
560     /* ------------------------------------------------------------ */
561     /**
562      * Customize an Exchange. Called when an exchange is about to be sent to
563      * allow Cookies and Credentials to be customized. Default implementation
564      * sets any cookies
565      */
566     protected void customize(HttpExchange exchange)
567     {
568         StringBuilder buf = null;
569         for (Cookie cookie : _cookies.values())
570         {
571             if (buf == null)
572                 buf = new StringBuilder();
573             else
574                 buf.append("; ");
575             buf.append(cookie.getName()); // TODO quotes
576             buf.append("=");
577             buf.append(cookie.getValue()); // TODO quotes
578         }
579         if (buf != null)
580             exchange.addRequestHeader(HttpHeaders.COOKIE,buf.toString());
581       
582         if (_scheme!=null)
583             exchange.setScheme(_scheme);
584     }
585 
586     /* ------------------------------------------------------------ */
587     public void setCookie(Cookie cookie)
588     {
589         _cookies.put(cookie.getName(),cookie);
590     }
591 
592     /* ------------------------------------------------------------ */
593     /* ------------------------------------------------------------ */
594     /* ------------------------------------------------------------ */
595     /**
596      * The base class for all bayeux exchanges.
597      */
598     protected class Exchange extends ContentExchange
599     {
600         Message[] _responses;
601         int _connectFailures;
602         int _backoffRetries = 0;
603         String _json;
604 
605         /* ------------------------------------------------------------ */
606         Exchange(String info)
607         {
608             setMethod("POST");
609             setScheme(HttpSchemes.HTTP_BUFFER);
610             setAddress(_cometdAddress);
611             setURI(_path + "/" + info);
612             setRequestContentType(_formEncoded?"application/x-www-form-urlencoded;charset=utf-8":"text/json;charset=utf-8");
613         }
614 
615         /* ------------------------------------------------------------ */
616         public int getBackoffRetries()
617         {
618             return _backoffRetries;
619         }
620 
621         /* ------------------------------------------------------------ */
622         public void incBackoffRetries()
623         {
624             ++_backoffRetries;
625         }
626 
627         /* ------------------------------------------------------------ */
628         protected void setMessage(String message)
629         {
630             message=extendOut(message);
631             setJson(message);
632         }
633 
634         /* ------------------------------------------------------------ */
635         protected void setJson(String json)
636         {
637             try
638             {
639                 _json = json;
640 
641                 if (_formEncoded)
642                     setRequestContent(new ByteArrayBuffer("message=" + URLEncoder.encode(_json,"utf-8")));
643                 else
644                     setRequestContent(new ByteArrayBuffer(_json,"utf-8"));
645             }
646             catch (Exception e)
647             {
648                 Log.ignore(e);
649                 setRequestContent(new ByteArrayBuffer(_json));
650             }
651         }
652 
653         /* ------------------------------------------------------------ */
654         protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
655         {
656             super.onResponseStatus(version,status,reason);
657         }
658 
659         /* ------------------------------------------------------------ */
660         protected void onResponseHeader(Buffer name, Buffer value) throws IOException
661         {
662             super.onResponseHeader(name,value);
663             if (!isRunning())
664                 return;
665 
666             if (HttpHeaders.CACHE.getOrdinal(name) == HttpHeaders.SET_COOKIE_ORDINAL)
667             {
668                 String cname = null;
669                 String cvalue = null;
670 
671                 QuotedStringTokenizer tok = new QuotedStringTokenizer(value.toString(),"=;",false,false);
672                 tok.setSingle(false);
673 
674                 if (tok.hasMoreElements())
675                     cname = tok.nextToken();
676                 if (tok.hasMoreElements())
677                     cvalue = tok.nextToken();
678 
679                 Cookie cookie = new Cookie(cname,cvalue);
680 
681                 while (tok.hasMoreTokens())
682                 {
683                     String token = tok.nextToken();
684                     if ("Version".equalsIgnoreCase(token))
685                         cookie.setVersion(Integer.parseInt(tok.nextToken()));
686                     else if ("Comment".equalsIgnoreCase(token))
687                         cookie.setComment(tok.nextToken());
688                     else if ("Path".equalsIgnoreCase(token))
689                         cookie.setPath(tok.nextToken());
690                     else if ("Domain".equalsIgnoreCase(token))
691                         cookie.setDomain(tok.nextToken());
692                     else if ("Expires".equalsIgnoreCase(token))
693                     {
694                         tok.nextToken();
695                         // TODO
696                     }
697                     else if ("Max-Age".equalsIgnoreCase(token))
698                     {
699                         tok.nextToken();
700                         // TODO
701                     }
702                     else if ("Secure".equalsIgnoreCase(token))
703                         cookie.setSecure(true);
704                 }
705 
706                 BayeuxClient.this.setCookie(cookie);
707             }
708         }
709 
710         /* ------------------------------------------------------------ */
711         protected void onResponseComplete() throws IOException
712         {
713             if (!isRunning())
714                 return;
715 
716             super.onResponseComplete();
717 
718             if (getResponseStatus() == 200)
719             {
720                 String content = getResponseContent();
721                 // TODO
722                 if (content == null || content.length() == 0)
723                     throw new IllegalStateException();
724                 _responses = _msgPool.parse(content);
725                 
726                 if (_responses!=null)
727                     for (int i=0;i<_responses.length;i++)
728                         extendIn(_responses[i]);
729             }
730         }
731 
732         /* ------------------------------------------------------------ */
733         protected void resend(boolean backoff)
734         {
735             if (!isRunning())
736                 return;
737 
738             final boolean disconnecting;
739             synchronized (_outQ)
740             {
741                 disconnecting=_disconnecting;
742             }
743             if (disconnecting)
744             {
745                 try{stop();}catch(Exception e){Log.ignore(e);}
746                 return;
747             }
748             
749             setJson(_json);
750             if (!send(this,backoff))
751                 Log.warn("Retries exhausted"); // giving up
752         }
753         
754         /* ------------------------------------------------------------ */
755         protected void recycle()
756         {
757             if (_responses!=null)
758                 for (Message msg:_responses)
759                     if (msg instanceof MessageImpl)
760                         ((MessageImpl)msg).decRef();
761             _responses=null;
762         }
763     }
764 
765     /* ------------------------------------------------------------ */
766     /**
767      * The Bayeux handshake exchange. Negotiates a client Id and initializes the
768      * protocol.
769      * 
770      */
771     protected class Handshake extends Exchange
772     {
773         public final static String __HANDSHAKE = "[{" + "\"channel\":\"/meta/handshake\"," + "\"version\":\"0.9\"," + "\"minimumVersion\":\"0.9\"" + "}]";
774 
775         Handshake()
776         {
777             super("handshake");
778             setMessage(__HANDSHAKE);
779         }
780 
781         /* ------------------------------------------------------------ */
782         /*
783          * (non-Javadoc)
784          * 
785          * @see
786          * org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete()
787          */
788         protected void onResponseComplete() throws IOException
789         {
790             super.onResponseComplete();
791 
792             if (!isRunning())
793                 return;    
794             
795             if (_disconnecting)
796             {
797                 Message error=_msgPool.newMessage();
798                 error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
799                 error.put("failure","expired");
800                 metaHandshake(false,false,error);
801                 try{stop();}catch(Exception e){Log.ignore(e);}
802                 return;
803             }
804 
805             if (getResponseStatus() == 200 && _responses != null && _responses.length > 0)
806             {
807                 MessageImpl response = (MessageImpl)_responses[0];
808                 boolean successful = response.isSuccessful();
809 
810                 // Get advice if there is any
811                 Map adviceField = (Map)response.get(Bayeux.ADVICE_FIELD);
812                 if (adviceField != null)
813                     _advice = new Advice(adviceField);
814 
815                 if (successful)
816                 {
817                     _handshook = true;
818                     if (Log.isDebugEnabled())
819                         Log.debug("Successful handshake, sending connect");
820                     _clientId = (String)response.get(Bayeux.CLIENT_FIELD);
821 
822                     metaHandshake(true,_handshook,response);
823                     _pull = new Connect();
824                     send(_pull,false);
825                 }
826                 else
827                 {
828                     metaHandshake(false,false,response);
829                     _handshook = false;
830                     if (_advice != null && _advice.isReconnectNone())
831                         throw new IOException("Handshake failed with advice reconnect=none :" + _responses[0]);
832                     else if (_advice != null && _advice.isReconnectHandshake())
833                     {
834                         _pull = new Handshake();
835                         if (!send(_pull,true))
836                             throw new IOException("Handshake, retries exhausted");
837                     }
838                     else
839                     // assume retry = reconnect?
840                     {
841                         _pull = new Connect();
842                         if (!send(_pull,true))
843                             throw new IOException("Connect after handshake, retries exhausted");
844                     }
845                 }
846             }
847             else
848             {
849                 Message error=_msgPool.newMessage();
850                 error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
851                 error.put("status",new Integer(getResponseStatus()));
852                 error.put("content",getResponseContent());
853                 
854                 metaHandshake(false,false,error);
855                 resend(true);
856             }
857             
858             recycle();
859         }
860 
861         /* ------------------------------------------------------------ */
862         protected void onExpire()
863         {
864             // super.onExpire();
865             Message error=_msgPool.newMessage();
866             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
867             error.put("failure","expired");
868             metaHandshake(false,false,error);
869             resend(true);
870         }
871 
872         /* ------------------------------------------------------------ */
873         protected void onConnectionFailed(Throwable ex)
874         {
875             // super.onConnectionFailed(ex);
876             Message error=_msgPool.newMessage();
877             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
878             error.put("failure",ex.toString());
879             error.put("exception",ex);
880             ex.printStackTrace();
881             metaHandshake(false,false,error);
882             resend(true);
883         }
884 
885         /* ------------------------------------------------------------ */
886         protected void onException(Throwable ex)
887         {
888             // super.onException(ex);
889             Message error=_msgPool.newMessage();
890             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
891             error.put("failure",ex.toString());
892             error.put("exception",ex);
893             metaHandshake(false,false,error);
894             resend(true);
895         }
896     }
897 
898     /* ------------------------------------------------------------ */
899     /**
900      * The Bayeux Connect exchange. Connect exchanges implement the long poll
901      * for Bayeux.
902      */
903     protected class Connect extends Exchange
904     {
905         String _connectString;
906 
907         Connect()
908         {
909             super("connect");
910             _connectString = "{" + "\"channel\":\"/meta/connect\"," + "\"clientId\":\"" + _clientId + "\"," + "\"connectionType\":\"long-polling\"" + "}";
911             setMessage(_connectString);
912         }
913 
914         protected void onResponseComplete() throws IOException
915         {
916             super.onResponseComplete();
917             if (!isRunning())
918                 return;
919 
920             if (getResponseStatus() == 200 && _responses != null && _responses.length > 0)
921             {
922                 try
923                 {
924                     startBatch();
925 
926                     for (int i = 0; i < _responses.length; i++)
927                     {
928                         Message msg = _responses[i];
929 
930                         // get advice if there is any
931                         Map adviceField = (Map)msg.get(Bayeux.ADVICE_FIELD);
932                         if (adviceField != null)
933                             _advice = new Advice(adviceField);
934 
935                         if (Bayeux.META_CONNECT.equals(msg.get(Bayeux.CHANNEL_FIELD)))
936                         {
937                             Boolean successful = (Boolean)msg.get(Bayeux.SUCCESSFUL_FIELD);
938                             if (successful != null && successful.booleanValue())
939                             {                               
940                                 metaConnect(true,msg);
941 
942                                 if (!isRunning())
943                                     break;
944                                 
945                                 synchronized (_outQ)
946                                 {
947                                     if (_disconnecting)
948                                         continue;
949                                     
950                                     if (!isInitialized())
951                                     {
952                                         setInitialized(true);
953                                         {
954                                             if (_outQ.size() > 0)
955                                             {
956                                                 _push = new Publish();
957                                                 send(_push);
958                                             }
959                                         }
960                                     }
961 
962                                 }
963                                 // send a Connect (ie longpoll) possibly with
964                                 // delay according to interval advice                                
965                                 _pull = new Connect();
966                                 send(_pull,false);
967                             }
968                             else
969                             {
970                                 // received a failure to our connect message,
971                                 // check the advice to see what to do:
972                                 // reconnect: none = hard error
973                                 // reconnect: handshake = send a handshake
974                                 // message
975                                 // reconnect: retry = send another connect,
976                                 // possibly using interval
977 
978                                 setInitialized(false);
979                                 metaConnect(false,msg);
980                                 
981                                 synchronized(_outQ)
982                                 {
983                                     if (!isRunning()||_disconnecting)
984                                         break;
985                                 }
986                                 
987                                 if (_advice != null && _advice.isReconnectNone())
988                                     throw new IOException("Connect failed, advice reconnect=none");
989                                 else if (_advice != null && _advice.isReconnectHandshake())
990                                 {
991                                     if (Log.isDebugEnabled())
992                                         Log.debug("connect received success=false, advice is to rehandshake");
993                                     _pull = new Handshake();
994                                     send(_pull,true);
995                                 }
996                                 else
997                                 {
998                                     // assume retry = reconnect
999                                     if (Log.isDebugEnabled())
1000                                         Log.debug("Assuming retry=reconnect");
1001                                     resend(true);
1002                                 }
1003                             }
1004                         }
1005                         deliver(null,msg);
1006                     }
1007                 }
1008                 finally
1009                 {
1010                     endBatch();
1011                 }
1012             }
1013             else
1014             {
1015                 Message error=_msgPool.newMessage();
1016                 error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1017                 error.put("status",getResponseStatus());
1018                 error.put("content",getResponseContent());
1019                 metaConnect(false,error);
1020                 resend(true);
1021             }
1022 
1023             recycle();
1024         }
1025 
1026         /* ------------------------------------------------------------ */
1027         protected void onExpire()
1028         {
1029             // super.onExpire();
1030             setInitialized(false);
1031             Message error=_msgPool.newMessage();
1032             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1033             error.put("failure","expired");
1034             metaConnect(false,error);
1035             resend(true);
1036         }
1037 
1038         /* ------------------------------------------------------------ */
1039         protected void onConnectionFailed(Throwable ex)
1040         {
1041             // super.onConnectionFailed(ex);
1042             setInitialized(false);
1043             Message error=_msgPool.newMessage();
1044             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1045             error.put("failure",ex.toString());
1046             error.put("exception",ex);
1047             metaConnect(false,error);
1048             resend(true);
1049         }
1050 
1051         /* ------------------------------------------------------------ */
1052         protected void onException(Throwable ex)
1053         {
1054             // super.onException(ex);
1055             setInitialized(false);
1056             Message error=_msgPool.newMessage();
1057             error.put(Bayeux.SUCCESSFUL_FIELD,Boolean.FALSE);
1058             error.put("failure",ex.toString());
1059             error.put("exception",ex);
1060             metaConnect(false,error);
1061             resend(true);
1062         }
1063     }
1064 
1065     /* ------------------------------------------------------------ */
1066     /**
1067      * Publish message exchange. Sends messages to bayeux server and handles any
1068      * messages received as a result.
1069      */
1070     protected class Publish extends Exchange
1071     {
1072         Publish()
1073         {
1074             super("publish");
1075             
1076             StringBuffer json = new StringBuffer(256);
1077             synchronized (json)
1078             {
1079                 synchronized (_outQ)
1080                 {
1081                     int s=_outQ.size();
1082                     if (s == 0)
1083                         return;
1084 
1085                     for (int i=0;i<s;i++)
1086                     {
1087                         Message message = _outQ.getUnsafe(i);
1088                         message.put(Bayeux.CLIENT_FIELD,_clientId);
1089                         extendOut(message);
1090 
1091                         json.append(i==0?'[':',');
1092                         _jsonOut.append(json,message);
1093 
1094                         if (message instanceof MessageImpl)
1095                             ((MessageImpl)message).decRef();
1096                     }
1097                     json.append(']');
1098                     _outQ.clear();
1099                     setJson(json.toString());
1100                 }
1101             }
1102         }
1103 
1104         protected Message[] getOutboundMessages()
1105         {
1106             try
1107             {
1108                 return _msgPool.parse(_json);
1109             }
1110             catch (IOException e)
1111             {
1112                 Log.warn("Error converting outbound messages");
1113                 if (Log.isDebugEnabled())
1114                     Log.debug(e);
1115                 return null;
1116             }
1117         }
1118 
1119         /* ------------------------------------------------------------ */
1120         /*
1121          * (non-Javadoc)
1122          * 
1123          * @see
1124          * org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete()
1125          */
1126         protected void onResponseComplete() throws IOException
1127         {
1128             super.onResponseComplete();
1129             try
1130             {
1131                 synchronized (_outQ)
1132                 {
1133                     startBatch();
1134                     _push = null;
1135                 }
1136 
1137                 if (getResponseStatus() == 200 && _responses != null && _responses.length > 0)
1138                 {
1139                     for (int i = 0; i < _responses.length; i++)
1140                     {
1141                         MessageImpl msg = (MessageImpl)_responses[i];
1142                             
1143                         deliver(null,msg);
1144                         if (Bayeux.META_DISCONNECT.equals(msg.getChannel())&&msg.isSuccessful())
1145                         {
1146                             if (isStarted())
1147                             {
1148                                 try{stop();}catch(Exception e){Log.ignore(e);}
1149                             }
1150                             break;
1151                         }
1152                     }
1153                 }
1154                 else
1155                 {
1156                     Log.warn("Publish, error=" + getResponseStatus());
1157                 }
1158             }
1159             finally
1160             {
1161                 endBatch();
1162             }
1163             recycle();
1164         }
1165 
1166         /* ------------------------------------------------------------ */
1167         protected void onExpire()
1168         {
1169             super.onExpire();
1170             metaPublishFail(null,this.getOutboundMessages());
1171             if (_disconnecting)
1172             {
1173                 try{stop();}catch(Exception e){Log.ignore(e);}
1174             }
1175         }
1176 
1177         /* ------------------------------------------------------------ */
1178         protected void onConnectionFailed(Throwable ex)
1179         {
1180             super.onConnectionFailed(ex);
1181             metaPublishFail(ex,this.getOutboundMessages());
1182             if (_disconnecting)
1183             {
1184                 try{stop();}catch(Exception e){Log.ignore(e);}
1185             }
1186         }
1187 
1188         /* ------------------------------------------------------------ */
1189         protected void onException(Throwable ex)
1190         {
1191             super.onException(ex);
1192             metaPublishFail(ex,this.getOutboundMessages());
1193             if (_disconnecting)
1194             {
1195                 try{stop();}catch(Exception e){Log.ignore(e);}
1196             }
1197         }
1198     }
1199 
1200     /* ------------------------------------------------------------ */
1201     public void addListener(ClientListener listener)
1202     {
1203         synchronized (_inQ)
1204         {
1205             boolean added=false;
1206             if (listener instanceof MessageListener)
1207             {
1208                 added=true;
1209                 if (_mListeners == null)
1210                     _mListeners = new ArrayList<MessageListener>();
1211                 _mListeners.add((MessageListener)listener);
1212             }
1213             if (listener instanceof RemoveListener)
1214             {
1215                 added=true;
1216                 if (_rListeners == null)
1217                     _rListeners = new ArrayList<RemoveListener>();
1218                 _rListeners.add((RemoveListener)listener);
1219             }
1220             
1221             if (!added)
1222                 throw new IllegalArgumentException();
1223         }
1224     }
1225 
1226     /* ------------------------------------------------------------ */
1227     public void removeListener(ClientListener listener)
1228     {
1229         synchronized (_inQ)
1230         {
1231             if (listener instanceof MessageListener)
1232             {
1233                 if (_mListeners != null)
1234                     _mListeners.remove((MessageListener)listener);
1235             }
1236             if (listener instanceof RemoveListener)
1237             {
1238                 if (_rListeners != null)
1239                     _rListeners.remove((RemoveListener)listener);
1240             }
1241         }
1242     }
1243 
1244     /* ------------------------------------------------------------ */
1245     public int getMaxQueue()
1246     {
1247         return -1;
1248     }
1249 
1250     /* ------------------------------------------------------------ */
1251     public Queue<Message> getQueue()
1252     {
1253         return _inQ;
1254     }
1255 
1256     /* ------------------------------------------------------------ */
1257     public void setMaxQueue(int max)
1258     {
1259         if (max != -1)
1260             throw new UnsupportedOperationException();
1261     }
1262 
1263     /* ------------------------------------------------------------ */
1264     /**
1265      * Send the exchange, possibly using a backoff.
1266      * 
1267      * @param exchange
1268      * @param backoff
1269      *            if true, use backoff algorithm to send
1270      * @return
1271      */
1272     protected boolean send(final Exchange exchange, final boolean backoff)
1273     {
1274         long interval = (_advice != null?_advice.getInterval():0);
1275 
1276         if (backoff)
1277         {
1278             int retries = exchange.getBackoffRetries();
1279             if (Log.isDebugEnabled())
1280                 Log.debug("Send with backoff, retries=" + retries + " for " + exchange);
1281             if (retries >= _backoffMaxRetries)
1282                 return false;
1283 
1284             exchange.incBackoffRetries();
1285             interval += (retries * _backoffInterval);
1286         }
1287 
1288         if (interval > 0)
1289         {
1290             TimerTask task = new TimerTask()
1291             {
1292                 public void run()
1293                 {
1294                     try
1295                     {
1296                         send(exchange);
1297                     }
1298                     catch (IOException e)
1299                     {
1300                         Log.warn("Delayed send, retry: "+e);
1301                         Log.debug(e);
1302                         send(exchange,true);
1303                     }
1304                 }
1305             };
1306             if (Log.isDebugEnabled())
1307                 Log.debug("Delay " + interval + " send of " + exchange);
1308             _timer.schedule(task,interval);
1309         }
1310         else
1311         {
1312             try
1313             {
1314                 send(exchange);
1315             }
1316             catch (IOException e)
1317             {
1318                 Log.warn("Send, retry on fail: "+e);
1319                 Log.debug(e);
1320                 return send(exchange,true);
1321             }
1322         }
1323         return true;
1324 
1325     }
1326 
1327     /* ------------------------------------------------------------ */
1328     /**
1329      * Send the exchange.
1330      * 
1331      * @param exchange
1332      * @throws IOException
1333      */
1334     protected void send(HttpExchange exchange) throws IOException
1335     {
1336         exchange.reset(); // ensure at start state
1337         customize(exchange);
1338         if (Log.isDebugEnabled())
1339             Log.debug("Send: using any connection=" + exchange);
1340         _httpClient.send(exchange); // use any connection
1341     }
1342 
1343     /* ------------------------------------------------------------ */
1344     /**
1345      * False when we have received a success=false message in response to a
1346      * Connect, or we have had an exception when sending or receiving a Connect.
1347      * 
1348      * True when handshake and then connect has happened.
1349      * 
1350      * @param b
1351      */
1352     protected void setInitialized(boolean b)
1353     {
1354         synchronized (_outQ)
1355         {
1356             _initialized = b;
1357         }
1358     }
1359 
1360     /* ------------------------------------------------------------ */
1361     protected boolean isInitialized()
1362     {
1363         return _initialized;
1364     }
1365 
1366     /* ------------------------------------------------------------ */
1367     /**
1368      * Called with the results of a /meta/connect message
1369      * @param success connect was returned with this status
1370      */
1371     protected void metaConnect(boolean success, Message message)
1372     {
1373         if (!success)
1374             Log.warn(this.toString()+" "+message.toString());
1375     }
1376 
1377     /* ------------------------------------------------------------ */
1378     /**
1379      * Called with the results of a /meta/handshake message
1380      * @param success connect was returned with this status
1381      * @param reestablish the client was previously connected.
1382      */
1383     protected void metaHandshake(boolean success, boolean reestablish, Message message)
1384     {
1385         if (!success)
1386             Log.warn(this.toString()+" "+message.toString());
1387     }
1388 
1389     /* ------------------------------------------------------------ */
1390     /**
1391      * Called with the results of a failed publish
1392      */
1393     protected void metaPublishFail(Throwable e, Message[] messages)
1394     {
1395         Log.warn(this.toString()+": "+e);
1396         Log.debug(e);
1397     }
1398 
1399     /* ------------------------------------------------------------ */
1400     /** Called to extend outbound string messages.
1401      * Some messages are sent as preformatted JSON strings (eg handshake
1402      * and connect messages).  This extendOut method is a variation of the
1403      * {@link #extendOut(Message)} method to efficiently cater for these
1404      * preformatted strings.
1405      * <p>
1406      * This method calls the {@link Extension}s added by {@link #addExtension(Extension)}
1407      * 
1408      * @param msg
1409      * @return the extended message
1410      */
1411     protected String extendOut(String msg)
1412     {
1413         if (_extensions==null)
1414             return msg;
1415         
1416         try
1417         {
1418             Message[] messages = _msgPool.parse(msg);
1419             for (int i=0; i<messages.length; i++)
1420                 extendOut(messages[i]);
1421             if (messages.length==1 && msg.charAt(0)=='{')
1422                 return _msgPool.getMsgJSON().toJSON(messages[0]);
1423             return _msgPool.getMsgJSON().toJSON(messages);
1424         }
1425         catch(IOException e)
1426         {
1427             Log.warn(e);
1428             return msg;
1429         }
1430     }
1431 
1432     /* ------------------------------------------------------------ */
1433     /** Called to extend outbound messages
1434      * <p>
1435      * This method calls the {@link Extension}s added by {@link #addExtension(Extension)}
1436      * 
1437      */
1438     protected void extendOut(Message message)
1439     {
1440         if (_extensions!=null)
1441         {
1442             Message m = message;
1443             if (m.getChannel().startsWith(Bayeux.META_SLASH))
1444                 for (int i=0;m!=null && i<_extensions.length;i++)
1445                     m=_extensions[i].sendMeta(this,m);
1446             else
1447                 for (int i=0;m!=null && i<_extensions.length;i++)
1448                     m=_extensions[i].send(this,m);
1449                 
1450             if (message!=m)
1451             {
1452                 message.clear();
1453                 if (m!=null)
1454                     for (Map.Entry<String,Object> entry:m.entrySet())
1455                         message.put(entry.getKey(),entry.getValue());
1456             }
1457         }
1458     }
1459 
1460     /* ------------------------------------------------------------ */
1461     /** Called to extend inbound messages
1462      * <p>
1463      * This method calls the {@link Extension}s added by {@link #addExtension(Extension)}
1464      * 
1465      */
1466     protected void extendIn(Message message)
1467     {
1468         if (_extensions!=null)
1469         {
1470             Message m = message;
1471             if (m.getChannel().startsWith(Bayeux.META_SLASH))
1472                 for (int i=_extensions.length;m!=null && i-->0;)
1473                     m=_extensions[i].rcvMeta(this,m);
1474             else
1475                 for (int i=_extensions.length;m!=null && i-->0;)
1476                     m=_extensions[i].rcv(this,m);
1477                 
1478             if (message!=m)
1479             {
1480                 message.clear();
1481                 if (m!=null)
1482                     for (Map.Entry<String,Object> entry:m.entrySet())
1483                         message.put(entry.getKey(),entry.getValue());
1484             }
1485         }
1486     }
1487 
1488     
1489 }