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