1
2
3
4
5
6
7
8
9
10
11
12
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
53
54
55
56
57
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;
75 private List<Message> _outQ;
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
93
94
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
137
138
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
156
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
182
183 public Listener getListener()
184 {
185 synchronized (_inQ)
186 {
187 return _listener;
188 }
189 }
190
191
192
193
194
195 public boolean hasMessages()
196 {
197 synchronized (_inQ)
198 {
199 return _inQ.size()>0;
200 }
201 }
202
203
204
205
206
207 public boolean isLocal()
208 {
209 return false;
210 }
211
212
213
214
215
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
230
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
244
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
256
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
268
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
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
306
307
308
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
322
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
339
340
341 public void startBatch()
342 {
343 synchronized (_outQ)
344 {
345 _batch++;
346 }
347 }
348
349
350
351
352
353
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());
365 buf.append("=");
366 buf.append(cookie.getValue());
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
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
474 }
475 else if ("Max-Age".equalsIgnoreCase(token))
476 {
477 tok.nextToken();
478
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
532
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
550 }
551 catch (IOException e)
552 {
553 _clientConnection=null;
554 Log.warn(e);
555 }
556 }
557
558
559
560
561
562 protected void onException(Throwable ex)
563 {
564 Log.warn("Handshake:"+ex);
565 Log.debug(ex);
566 }
567
568
569
570
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
596
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
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
673
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
700
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 }