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.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
55
56
57
58
59
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;
77 private Queue<Message> _outQ;
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
95
96
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
139
140
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
158
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
184
185 public Listener getListener()
186 {
187 synchronized (_inQ)
188 {
189 return _listener;
190 }
191 }
192
193
194
195
196
197 public boolean hasMessages()
198 {
199 synchronized (_inQ)
200 {
201 return _inQ.size()>0;
202 }
203 }
204
205
206
207
208
209 public boolean isLocal()
210 {
211 return false;
212 }
213
214
215
216
217
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
232
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
246
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
258
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
270
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
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
308
309
310
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
324
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
341
342
343 public void startBatch()
344 {
345 synchronized (_outQ)
346 {
347 _batch++;
348 }
349 }
350
351
352
353
354
355
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());
367 buf.append("=");
368 buf.append(cookie.getValue());
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
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
476 }
477 else if ("Max-Age".equalsIgnoreCase(token))
478 {
479 tok.nextToken();
480
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
537
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
555 }
556 catch (IOException e)
557 {
558 _clientConnection=null;
559 Log.warn(e);
560 }
561 }
562
563
564
565
566
567 protected void onException(Throwable ex)
568 {
569 Log.warn("Handshake:"+ex);
570 Log.debug(ex);
571 }
572
573
574
575
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
601
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
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
678
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
705
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 }