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