1   // ========================================================================
2   // Copyright 2006-2007 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.InetSocketAddress;
19  import java.net.URLEncoder;
20  import java.net.UnknownHostException;
21  import java.util.ArrayList;
22  import java.util.EventListener;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.concurrent.ConcurrentHashMap;
27  
28  import javax.servlet.http.Cookie;
29  
30  import org.mortbay.cometd.MessageImpl;
31  import org.mortbay.cometd.MessagePool;
32  import org.mortbay.io.Buffer;
33  import org.mortbay.io.ByteArrayBuffer;
34  import org.mortbay.jetty.HttpHeaders;
35  import org.mortbay.jetty.HttpSchemes;
36  import org.mortbay.jetty.client.HttpClient;
37  import org.mortbay.jetty.client.HttpConnection;
38  import org.mortbay.jetty.client.HttpDestination;
39  import org.mortbay.jetty.client.HttpExchange;
40  import org.mortbay.log.Log;
41  import org.mortbay.util.QuotedStringTokenizer;
42  import org.mortbay.util.ajax.JSON;
43  
44  import dojox.cometd.Bayeux;
45  import dojox.cometd.Client;
46  import dojox.cometd.Listener;
47  import dojox.cometd.RemoveListener;
48  import dojox.cometd.Message;
49  import dojox.cometd.MessageListener;
50  
51  /* ------------------------------------------------------------ */
52  /** Bayeux protocol Client.
53   * <p>
54   * Implements a Bayeux Ajax Push client as part of the cometd project.
55   * 
56   * @see http://cometd.com
57   * @author gregw
58   *
59   */
60  public class BayeuxClient extends MessagePool implements Client
61  {
62      private HttpClient _client;
63      private HttpConnection _clientConnection;
64      private InetSocketAddress _address;
65      private HttpExchange _pull;
66      private HttpExchange _push;
67      private String _uri="/cometd";
68      private boolean _initialized=false;
69      private boolean _disconnecting=false;
70      private String _clientId;
71      private Listener _listener;
72      private List<RemoveListener> _rListeners;
73      private List<MessageListener> _mListeners;
74      private List<Message> _inQ;  // queue of incoming messages used if no listener available. Used as the lock object for all incoming operations.
75      private List<Message> _outQ; // queue of outgoing messages. Used as the lock object for all outgoing operations.
76      private int _batch;
77      private boolean _formEncoded;
78      private Map<String, Cookie> _cookies=new ConcurrentHashMap<String, Cookie>();
79  
80      /* ------------------------------------------------------------ */
81      public BayeuxClient(HttpClient client, InetSocketAddress address, String uri) throws IOException
82      {
83          _client=client;
84          _address=address;
85          _uri=uri;
86  
87          _inQ=new LinkedList<Message>();
88          _outQ=new LinkedList<Message>();
89      }
90  
91      /* ------------------------------------------------------------ */
92      /* (non-Javadoc)
93       * Returns the clientId
94       * @see dojox.cometd.Client#getId()
95       */
96      public String getId()
97      {
98          return _clientId;
99      }
100 
101     /* ------------------------------------------------------------ */
102     public void start() throws UnknownHostException, IOException
103     {
104         synchronized (_outQ)
105         {
106             if (!_initialized && _pull==null)
107                 _pull=new Handshake();
108         }
109     }
110     
111     /* ------------------------------------------------------------ */
112     private void checkConnection() throws UnknownHostException, IOException
113     {
114         synchronized (_outQ)
115         {
116             if (_clientConnection==null)
117             {
118                 HttpDestination destination = _client.getDestination(_address,false);
119                 _clientConnection=destination.getConnection();
120                 if (_clientConnection==null)
121                     throw new IOException("unable to open connection to "+_address);
122             }
123         }
124     }
125     
126     /* ------------------------------------------------------------ */
127     public boolean isPolling()
128     {
129         synchronized (_outQ)
130         {
131             return _pull!=null;
132         }
133     }
134 
135     /* ------------------------------------------------------------ */
136     /** (non-Javadoc)
137      * @deprecated use {@link #deliver(Client, String, Object, String)}
138      * @see dojox.cometd.Client#deliver(dojox.cometd.Client, java.util.Map)
139      */
140     public void deliver(Client from, Message message)
141     {
142         synchronized (_inQ)
143         {
144             if (_mListeners==null)
145                 _inQ.add(message);
146             else
147             {
148                 for (MessageListener l : _mListeners)
149                     l.deliver(from,this,message);
150             }
151         }
152     }
153 
154     /* ------------------------------------------------------------ */
155     /* (non-Javadoc)
156      * @see dojox.cometd.Client#deliver(dojox.cometd.Client, java.lang.String, java.lang.Object, java.lang.String)
157      */
158     public void deliver(Client from, String toChannel, Object data, String id)
159     {
160         Message message = new MessageImpl();
161 
162         message.put(Bayeux.CHANNEL_FIELD,toChannel);
163         message.put(Bayeux.DATA_FIELD,data);
164         if (id!=null)   
165             message.put(Bayeux.ID_FIELD,id);
166         
167         synchronized (_inQ)
168         {
169             if (_mListeners==null)
170                 _inQ.add(message);
171             else
172             {
173                 for (MessageListener l : _mListeners)
174                     l.deliver(from,this,message);
175             }
176         }
177     }
178 
179     /* ------------------------------------------------------------ */
180     /**
181      * @deprecated
182      */
183     public Listener getListener()
184     {
185         synchronized (_inQ)
186         {
187             return _listener;
188         }
189     }
190 
191     /* ------------------------------------------------------------ */
192     /* (non-Javadoc)
193      * @see dojox.cometd.Client#hasMessages()
194      */
195     public boolean hasMessages()
196     {
197         synchronized (_inQ)
198         {
199             return _inQ.size()>0;
200         }
201     }
202 
203     /* ------------------------------------------------------------ */
204     /* (non-Javadoc)
205      * @see dojox.cometd.Client#isLocal()
206      */
207     public boolean isLocal()
208     {
209         return false;
210     }
211 
212 
213     /* ------------------------------------------------------------ */
214     /* (non-Javadoc)
215      * @see dojox.cometd.Client#subscribe(java.lang.String)
216      */
217     private void publish(Message msg)
218     {
219         synchronized (_outQ)
220         {
221             _outQ.add(msg);
222 
223             if (_batch==0&&_initialized&&_push==null)
224                 _push=new Publish();
225         }
226     }
227     
228     /* ------------------------------------------------------------ */
229     /* (non-Javadoc)
230      * @see dojox.cometd.Client#publish(java.lang.String, java.lang.Object, java.lang.String)
231      */
232     public void publish(String toChannel, Object data, String msgId)
233     {
234         Message msg=new MessageImpl();
235         msg.put(Bayeux.CHANNEL_FIELD,toChannel);
236         msg.put(Bayeux.DATA_FIELD,data);
237         if (msgId!=null)
238             msg.put(Bayeux.ID_FIELD,msgId);
239         publish(msg);
240     }
241     
242     /* ------------------------------------------------------------ */
243     /* (non-Javadoc)
244      * @see dojox.cometd.Client#subscribe(java.lang.String)
245      */
246     public void subscribe(String toChannel)
247     {
248         Message msg=new MessageImpl();
249         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_SUBSCRIBE);
250         msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel);
251         publish(msg);
252     }
253 
254     /* ------------------------------------------------------------ */
255     /* (non-Javadoc)
256      * @see dojox.cometd.Client#unsubscribe(java.lang.String)
257      */
258     public void unsubscribe(String toChannel)
259     {
260         Message msg=new MessageImpl();
261         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_UNSUBSCRIBE);
262         msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel);
263         publish(msg);
264     }
265 
266     /* ------------------------------------------------------------ */
267     /* (non-Javadoc)
268      * @see dojox.cometd.Client#remove(boolean)
269      */
270     public void remove(boolean timeout)
271     {
272         Message msg=new MessageImpl();
273         msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_DISCONNECT);
274 
275         synchronized (_outQ)
276         {
277             _outQ.add(msg);
278 
279             _initialized=false;
280             _disconnecting=true;
281 
282             if (_batch==0&&_initialized&&_push==null)
283                 _push=new Publish();
284 
285         }
286     }
287 
288     /* ------------------------------------------------------------ */
289     /**
290      * @deprecated
291      */
292     public void setListener(Listener listener)
293     {
294         synchronized (_inQ)
295         {
296             if (_listener!=null)
297                 removeListener(_listener);
298             _listener=listener;
299             if (_listener!=null)
300                 addListener(_listener);
301         }
302     }
303 
304     /* ------------------------------------------------------------ */
305     /* (non-Javadoc)
306      * Removes all available messages from the inbound queue.
307      * If a listener is set then messages are not queued.
308      * @see dojox.cometd.Client#takeMessages()
309      */
310     public List<Message> takeMessages()
311     {
312         synchronized (_inQ)
313         {
314             LinkedList<Message> list=new LinkedList<Message>(_inQ);
315             _inQ.clear();
316             return list;
317         }
318     }
319 
320     /* ------------------------------------------------------------ */
321     /* (non-Javadoc)
322      * @see dojox.cometd.Client#endBatch()
323      */
324     public void endBatch()
325     {
326         synchronized (_outQ)
327         {
328             if (--_batch<=0)
329             {
330                 _batch=0;
331                 if ((_initialized||_disconnecting)&&_push==null&&_outQ.size()>0)
332                     _push=new Publish();
333             }
334         }
335     }
336 
337     /* ------------------------------------------------------------ */
338     /* (non-Javadoc)
339      * @see dojox.cometd.Client#startBatch()
340      */
341     public void startBatch()
342     {
343         synchronized (_outQ)
344         {
345             _batch++;
346         }
347     }
348 
349     /* ------------------------------------------------------------ */
350     /** Customize an Exchange.
351      * Called when an exchange is about to be sent to allow Cookies
352      * and Credentials to be customized.  Default implementation sets
353      * any cookies 
354      */
355     protected void customize(HttpExchange exchange)
356     {
357         StringBuilder buf=null;
358         for (Cookie cookie : _cookies.values())
359         {
360 	    if (buf==null)
361 	        buf=new StringBuilder();
362             else
363 	        buf.append("; ");
364 	    buf.append(cookie.getName()); // TODO quotes
365 	    buf.append("=");
366 	    buf.append(cookie.getValue()); // TODO quotes
367         }
368 	if (buf!=null)
369             exchange.addRequestHeader(HttpHeaders.COOKIE,buf.toString());
370     }
371 
372     /* ------------------------------------------------------------ */
373     public void setCookie(Cookie cookie)
374     {
375         _cookies.put(cookie.getName(),cookie);
376     }
377 
378     /* ------------------------------------------------------------ */
379     /** The base class for all bayeux exchanges.
380      */
381     private class Exchange extends HttpExchange.ContentExchange
382     {
383         Object[] _responses;
384         int _connectFailures;
385 
386         Exchange(String info)
387         {
388             setMethod("POST");
389             setScheme(HttpSchemes.HTTP_BUFFER);
390             setAddress(_address);
391             setURI(_uri+"/"+info);
392 
393             setRequestContentType(_formEncoded?"application/x-www-form-urlencoded;charset=utf-8":"text/json;charset=utf-8");
394         }
395 
396         protected void setMessage(String message)
397         {
398             try
399             {
400                 if (_formEncoded)
401                     setRequestContent(new ByteArrayBuffer("message="+URLEncoder.encode(message,"utf-8")));
402                 else
403                     setRequestContent(new ByteArrayBuffer(message,"utf-8"));
404             }
405             catch (Exception e)
406             {
407                 Log.warn(e);
408             }
409         }
410 
411         protected void setMessages(List<Message> messages)
412         {
413             try
414             {
415                 for (Message msg : messages)
416                 {
417                     msg.put(Bayeux.CLIENT_FIELD,_clientId);
418                 }
419                 String json=JSON.toString(messages);
420 
421                 if (_formEncoded)
422                     setRequestContent(new ByteArrayBuffer("message="+URLEncoder.encode(json,"utf-8")));
423                 else
424                     setRequestContent(new ByteArrayBuffer(json,"utf-8"));
425 
426             }
427             catch (Exception e)
428             {
429                 Log.warn(e);
430             }
431 
432         }
433 
434         /* ------------------------------------------------------------ */
435         protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
436         {
437             super.onResponseStatus(version,status,reason);
438         }
439 
440         /* ------------------------------------------------------------ */
441         protected void onResponseHeader(Buffer name, Buffer value) throws IOException
442         {
443             super.onResponseHeader(name,value);
444             if (HttpHeaders.CACHE.getOrdinal(name)==HttpHeaders.SET_COOKIE_ORDINAL)
445             {
446                 String cname=null;
447                 String cvalue=null;
448 
449                 QuotedStringTokenizer tok=new QuotedStringTokenizer(value.toString(),"=;",false,false);
450                 tok.setSingle(false);
451 
452                 if (tok.hasMoreElements())
453                     cname=tok.nextToken();
454                 if (tok.hasMoreElements())
455                     cvalue=tok.nextToken();
456 
457                 Cookie cookie=new Cookie(cname,cvalue);
458 
459                 while (tok.hasMoreTokens())
460                 {
461                     String token=tok.nextToken();
462                     if ("Version".equalsIgnoreCase(token))
463                         cookie.setVersion(Integer.parseInt(tok.nextToken()));
464                     else if ("Comment".equalsIgnoreCase(token))
465                         cookie.setComment(tok.nextToken());
466                     else if ("Path".equalsIgnoreCase(token))
467                         cookie.setPath(tok.nextToken());
468                     else if ("Domain".equalsIgnoreCase(token))
469                         cookie.setDomain(tok.nextToken());
470                     else if ("Expires".equalsIgnoreCase(token))
471                     {
472                         tok.nextToken();
473                         // TODO
474                     }
475                     else if ("Max-Age".equalsIgnoreCase(token))
476                     {
477                         tok.nextToken();
478                         // TODO
479                     }
480                     else if ("Secure".equalsIgnoreCase(token))
481                         cookie.setSecure(true);
482                 }
483 
484                 BayeuxClient.this.setCookie(cookie);
485             }
486         }
487 
488         /* ------------------------------------------------------------ */
489         protected void onResponseComplete() throws IOException
490         {
491             super.onResponseComplete();
492 
493             if (getResponseStatus()==200)
494             {
495                 _responses=parse(getResponseContent());
496             }
497         }
498 
499         /* ------------------------------------------------------------ */
500         protected void onExpire()
501         {
502             super.onExpire();
503         }
504 
505         /* ------------------------------------------------------------ */
506         protected void onConnectionFailed(Throwable ex)
507         {
508             super.onConnectionFailed(ex);
509             if (++_connectFailures<5)
510             {
511                 try
512                 {
513                     _client.send(this);
514                 }
515                 catch (IOException e)
516                 {
517                     Log.warn(e);
518                 }
519             }
520         }
521 
522         /* ------------------------------------------------------------ */
523         protected void onException(Throwable ex)
524         {
525             super.onException(ex);
526         }
527 
528     }
529 
530     /* ------------------------------------------------------------ */
531     /** The Bayeux handshake exchange.
532      * Negotiates a client Id and initializes the protocol.
533      *
534      */
535     private class Handshake extends Exchange
536     {
537         final static String __HANDSHAKE="[{"+"\"channel\":\"/meta/handshake\","+"\"version\":\"0.9\","+"\"minimumVersion\":\"0.9\""+"}]";
538 
539         Handshake()
540         {
541             super("handshake");
542             setMessage(__HANDSHAKE);
543 
544             try
545             {
546                 customize(this);
547                 checkConnection();
548                 _clientConnection.send(this);
549                 //_client.send(this);
550             }
551             catch (IOException e)
552             {
553                 _clientConnection=null;
554                 Log.warn(e);
555             }
556         }
557 
558         /* ------------------------------------------------------------ */
559         /* (non-Javadoc)
560          * @see org.mortbay.jetty.client.HttpExchange#onException(java.lang.Throwable)
561          */
562         protected void onException(Throwable ex)
563         {
564             Log.warn("Handshake:"+ex);
565             Log.debug(ex);
566         }
567 
568         /* ------------------------------------------------------------ */
569         /* (non-Javadoc)
570          * @see org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete()
571          */
572         protected void onResponseComplete() throws IOException
573         {
574             super.onResponseComplete();
575             if (getResponseStatus()==200&&_responses!=null&&_responses.length>0)
576             {
577                 Map<?,?> response=(Map<?,?>)_responses[0];
578                 Boolean successful=(Boolean)response.get(Bayeux.SUCCESSFUL_FIELD);
579                 if (successful!=null&&successful.booleanValue())
580                 {
581                     _clientId=(String)response.get(Bayeux.CLIENT_FIELD);
582                     _pull=new Connect();
583                 }
584                 else
585                     throw new IOException("Handshake failed:"+_responses[0]);
586             }
587             else
588             {
589                 throw new IOException("Handshake failed: "+getResponseStatus());
590             }
591         }
592     }
593 
594     /* ------------------------------------------------------------ */
595     /** The Bayeux Connect exchange.
596      * Connect exchanges implement the long poll for Bayeux.
597      */
598     private class Connect extends Exchange
599     {
600         Connect()
601         {
602             super("connect");
603             String connect="{"+"\"channel\":\"/meta/connect\","+"\"clientId\":\""+_clientId+"\","+"\"connectionType\":\"long-polling\""+"}";
604             setMessage(connect);
605 
606             try
607             {
608                 customize(this);
609                 checkConnection();
610                 _clientConnection.send(this);
611                 // _client.send(this);
612             }
613             catch (IOException e)
614             {
615                 _clientConnection=null;
616                 Log.warn(e);
617             }
618         }
619 
620         protected void onResponseComplete() throws IOException
621         {
622             super.onResponseComplete();
623             if (getResponseStatus()==200&&_responses!=null&&_responses.length>0)
624             {
625                 try
626                 {
627                     startBatch();
628 
629                     for (int i=0; i<_responses.length; i++)
630                     {
631                         Message msg=(Message)_responses[i];
632 
633                         if (Bayeux.META_CONNECT.equals(msg.get(Bayeux.CHANNEL_FIELD)))
634                         {
635                             Boolean successful=(Boolean)msg.get(Bayeux.SUCCESSFUL_FIELD);
636                             if (successful!=null&&successful.booleanValue())
637                             {
638                                 if (!_initialized)
639                                 {
640                                     _initialized=true;
641                                     synchronized (_outQ)
642                                     {
643                                         if (_outQ.size()>0)
644                                             _push=new Publish();
645                                     }
646                                 }
647 
648                                 _pull=new Connect();
649                             }
650                             else
651                                 throw new IOException("Connect failed:"+_responses[0]);
652                         }
653 
654                         deliver(null,msg);
655                     }
656                 }
657                 finally
658                 {
659                     endBatch();
660                 }
661 
662             }
663             else
664             {
665                 throw new IOException("Connect failed: "+getResponseStatus());
666             }
667         }
668     }
669 
670     /* ------------------------------------------------------------ */
671     /** 
672      * Publish message exchange.
673      * Sends messages to bayeux server and handles any messages received as a result.
674      */
675     private class Publish extends Exchange
676     {
677         Publish()
678         {
679             super("publish");
680             synchronized (_outQ)
681             {
682                 if (_outQ.size()==0)
683                     return;
684                 setMessages(_outQ);
685                 _outQ.clear();
686             }
687             try
688             {
689                 customize(this);
690                 _client.send(this);
691             }
692             catch (IOException e)
693             {
694                 Log.warn(e);
695             }
696         }
697 
698         /* ------------------------------------------------------------ */
699         /* (non-Javadoc)
700          * @see org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete()
701          */
702         protected void onResponseComplete() throws IOException
703         {
704             super.onResponseComplete();
705 
706             try
707             {
708                 synchronized (_outQ)
709                 {
710                     startBatch();
711                     _push=null;
712                 }
713 
714                 if (getResponseStatus()==200&&_responses!=null&&_responses.length>0)
715                 {
716 
717                     for (int i=0; i<_responses.length; i++)
718                     {
719                         Message msg=(Message)_responses[i];
720                         deliver(null,msg);
721                     }
722                 }
723                 else
724                 {
725                     throw new IOException("Reconnect failed: "+getResponseStatus());
726                 }
727             }
728             finally
729             {
730                 endBatch();
731             }
732         }
733     }
734 
735     public void addListener(EventListener listener)
736     {
737         synchronized(_inQ)
738         {
739             if (listener instanceof MessageListener)
740             {
741                 if (_mListeners==null)
742                     _mListeners=new ArrayList<MessageListener>();
743                 _mListeners.add((MessageListener)listener);
744             }
745             if (listener instanceof RemoveListener)
746             {
747                 if (_rListeners==null)
748                     _rListeners=new ArrayList<RemoveListener>();
749                 _rListeners.add((RemoveListener)listener);
750             }
751         }
752     }
753 
754     public void removeListener(EventListener listener)
755     {
756         synchronized(_inQ)
757         {
758             if (listener instanceof MessageListener)
759             {
760                 if (_mListeners!=null)
761                     _mListeners.remove((MessageListener)listener);
762             }
763             if (listener instanceof RemoveListener)
764             {
765                 if (_rListeners!=null)
766                     _rListeners.remove((RemoveListener)listener);
767             }
768         }
769     }
770 
771 }