View Javadoc

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