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.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
52
53
54
55
56
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;
73 private Queue<Message> _outQ;
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
91
92
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
120
121
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
139
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
165
166 public Listener getListener()
167 {
168 synchronized (_inQ)
169 {
170 return _listener;
171 }
172 }
173
174
175
176
177
178 public boolean hasMessages()
179 {
180 synchronized (_inQ)
181 {
182 return _inQ.size()>0;
183 }
184 }
185
186
187
188
189
190 public boolean isLocal()
191 {
192 return false;
193 }
194
195
196
197
198
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
213
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
227
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
239
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
251
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
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
289
290
291
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
305
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
322
323
324 public void startBatch()
325 {
326 synchronized (_outQ)
327 {
328 _batch++;
329 }
330 }
331
332
333
334
335
336
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());
348 buf.append("=");
349 buf.append(cookie.getValue());
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
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
457 }
458 else if ("Max-Age".equalsIgnoreCase(token))
459 {
460 tok.nextToken();
461
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
518
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
543
544
545 protected void onException(Throwable ex)
546 {
547 Log.warn("Handshake:"+ex);
548 Log.debug(ex);
549 }
550
551
552
553
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
579
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
653
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
680
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 }