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.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
53
54
55
56
57
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;
74 private Queue<Message> _outQ;
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
92
93
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
121
122
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
140
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
166
167 public Listener getListener()
168 {
169 synchronized (_inQ)
170 {
171 return _listener;
172 }
173 }
174
175
176
177
178
179 public boolean hasMessages()
180 {
181 synchronized (_inQ)
182 {
183 return _inQ.size()>0;
184 }
185 }
186
187
188
189
190
191 public boolean isLocal()
192 {
193 return false;
194 }
195
196
197
198
199
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
214
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
228
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
240
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
252
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
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
290
291
292
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
306
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
323
324
325 public void startBatch()
326 {
327 synchronized (_outQ)
328 {
329 _batch++;
330 }
331 }
332
333
334
335
336
337
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());
349 buf.append("=");
350 buf.append(cookie.getValue());
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
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
458 }
459 else if ("Max-Age".equalsIgnoreCase(token))
460 {
461 tok.nextToken();
462
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
519
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
544
545
546 protected void onException(Throwable ex)
547 {
548 Log.warn("Handshake:"+ex);
549 Log.debug(ex);
550 }
551
552
553
554
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
580
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
654
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
681
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 }