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