// ======================================================================== // Copyright 2006-2007 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ======================================================================== package org.mortbay.cometd.client; import java.io.IOException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.Cookie; import org.cometd.Bayeux; import org.cometd.Client; import org.cometd.ClientListener; import org.cometd.Listener; import org.cometd.Message; import org.cometd.MessageListener; import org.cometd.RemoveListener; import org.mortbay.cometd.MessageImpl; import org.mortbay.cometd.MessagePool; import org.mortbay.io.Buffer; import org.mortbay.io.ByteArrayBuffer; import org.mortbay.jetty.HttpHeaders; import org.mortbay.jetty.HttpSchemes; import org.mortbay.jetty.client.Address; import org.mortbay.jetty.client.HttpClient; import org.mortbay.jetty.client.HttpExchange; import org.mortbay.log.Log; import org.mortbay.util.ArrayQueue; import org.mortbay.util.QuotedStringTokenizer; import org.mortbay.util.ajax.JSON; /* ------------------------------------------------------------ */ /** Bayeux protocol Client. * <p> * Implements a Bayeux Ajax Push client as part of the cometd project. * * @see http://cometd.com * @author gregw * */ public class BayeuxClient extends MessagePool implements Client { private HttpClient _client; private Address _address; private HttpExchange _pull; private HttpExchange _push; private String _uri="/cometd"; private boolean _initialized=false; private boolean _disconnecting=false; private String _clientId; private Listener _listener; private List<RemoveListener> _rListeners; private List<MessageListener> _mListeners; private Queue<Message> _inQ; // queue of incoming messages used if no listener available. Used as the lock object for all incoming operations. private Queue<Message> _outQ; // queue of outgoing messages. Used as the lock object for all outgoing operations. private int _batch; private boolean _formEncoded; private Map<String, Cookie> _cookies=new ConcurrentHashMap<String, Cookie>(); /* ------------------------------------------------------------ */ public BayeuxClient(HttpClient client, Address address, String uri) throws IOException { _client=client; _address=address; _uri=uri; _inQ=new ArrayQueue<Message>(); _outQ=new ArrayQueue<Message>(); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * Returns the clientId * @see dojox.cometd.Client#getId() */ public String getId() { return _clientId; } /* ------------------------------------------------------------ */ public void start() { synchronized (_outQ) { if (!_initialized && _pull==null) _pull=new Handshake(); } } /* ------------------------------------------------------------ */ public boolean isPolling() { synchronized (_outQ) { return _pull!=null; } } /* ------------------------------------------------------------ */ /** (non-Javadoc) * @deprecated use {@link #deliver(Client, String, Object, String)} * @see org.cometd.Client#deliver(org.cometd.Client, java.util.Map) */ public void deliver(Client from, Message message) { synchronized (_inQ) { if (_mListeners==null) _inQ.add(message); else { for (MessageListener l : _mListeners) l.deliver(from,this,message); } } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#deliver(dojox.cometd.Client, java.lang.String, java.lang.Object, java.lang.String) */ public void deliver(Client from, String toChannel, Object data, String id) { Message message = new MessageImpl(); message.put(Bayeux.CHANNEL_FIELD,toChannel); message.put(Bayeux.DATA_FIELD,data); if (id!=null) message.put(Bayeux.ID_FIELD,id); synchronized (_inQ) { if (_mListeners==null) _inQ.add(message); else { for (MessageListener l : _mListeners) l.deliver(from,this,message); } } } /* ------------------------------------------------------------ */ /** * @deprecated */ public Listener getListener() { synchronized (_inQ) { return _listener; } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#hasMessages() */ public boolean hasMessages() { synchronized (_inQ) { return _inQ.size()>0; } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#isLocal() */ public boolean isLocal() { return false; } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#subscribe(java.lang.String) */ private void publish(Message msg) { synchronized (_outQ) { _outQ.add(msg); if (_batch==0&&_initialized&&_push==null) _push=new Publish(); } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#publish(java.lang.String, java.lang.Object, java.lang.String) */ public void publish(String toChannel, Object data, String msgId) { Message msg=new MessageImpl(); msg.put(Bayeux.CHANNEL_FIELD,toChannel); msg.put(Bayeux.DATA_FIELD,data); if (msgId!=null) msg.put(Bayeux.ID_FIELD,msgId); publish(msg); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#subscribe(java.lang.String) */ public void subscribe(String toChannel) { Message msg=new MessageImpl(); msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_SUBSCRIBE); msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel); publish(msg); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#unsubscribe(java.lang.String) */ public void unsubscribe(String toChannel) { Message msg=new MessageImpl(); msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_UNSUBSCRIBE); msg.put(Bayeux.SUBSCRIPTION_FIELD,toChannel); publish(msg); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#remove(boolean) */ public void remove(boolean timeout) { Message msg=new MessageImpl(); msg.put(Bayeux.CHANNEL_FIELD,Bayeux.META_DISCONNECT); synchronized (_outQ) { _outQ.add(msg); _initialized=false; _disconnecting=true; if (_batch==0&&_initialized&&_push==null) _push=new Publish(); } } /* ------------------------------------------------------------ */ /** * @deprecated */ public void setListener(Listener listener) { synchronized (_inQ) { if (_listener!=null) removeListener(_listener); _listener=listener; if (_listener!=null) addListener(_listener); } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * Removes all available messages from the inbound queue. * If a listener is set then messages are not queued. * @see dojox.cometd.Client#takeMessages() */ public List<Message> takeMessages() { synchronized (_inQ) { LinkedList<Message> list=new LinkedList<Message>(_inQ); _inQ.clear(); return list; } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#endBatch() */ public void endBatch() { synchronized (_outQ) { if (--_batch<=0) { _batch=0; if ((_initialized||_disconnecting)&&_push==null&&_outQ.size()>0) _push=new Publish(); } } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see dojox.cometd.Client#startBatch() */ public void startBatch() { synchronized (_outQ) { _batch++; } } /* ------------------------------------------------------------ */ /** Customize an Exchange. * Called when an exchange is about to be sent to allow Cookies * and Credentials to be customized. Default implementation sets * any cookies */ protected void customize(HttpExchange exchange) { StringBuilder buf=null; for (Cookie cookie : _cookies.values()) { if (buf==null) buf=new StringBuilder(); else buf.append("; "); buf.append(cookie.getName()); // TODO quotes buf.append("="); buf.append(cookie.getValue()); // TODO quotes } if (buf!=null) exchange.addRequestHeader(HttpHeaders.COOKIE,buf.toString()); } /* ------------------------------------------------------------ */ public void setCookie(Cookie cookie) { _cookies.put(cookie.getName(),cookie); } /* ------------------------------------------------------------ */ /** The base class for all bayeux exchanges. */ private class Exchange extends HttpExchange.ContentExchange { Object[] _responses; int _connectFailures; Exchange(String info) { setMethod("POST"); setScheme(HttpSchemes.HTTP_BUFFER); setAddress(_address); setURI(_uri+"/"+info); setRequestContentType(_formEncoded?"application/x-www-form-urlencoded;charset=utf-8":"text/json;charset=utf-8"); } protected void setMessage(String message) { try { if (_formEncoded) setRequestContent(new ByteArrayBuffer("message="+URLEncoder.encode(message,"utf-8"))); else setRequestContent(new ByteArrayBuffer(message,"utf-8")); } catch (Exception e) { Log.warn(e); } } protected void setMessages(Queue<Message> messages) { try { for (Message msg : messages) { msg.put(Bayeux.CLIENT_FIELD,_clientId); } String json=JSON.toString(messages); if (_formEncoded) setRequestContent(new ByteArrayBuffer("message="+URLEncoder.encode(json,"utf-8"))); else setRequestContent(new ByteArrayBuffer(json,"utf-8")); } catch (Exception e) { Log.warn(e); } } /* ------------------------------------------------------------ */ protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException { super.onResponseStatus(version,status,reason); } /* ------------------------------------------------------------ */ protected void onResponseHeader(Buffer name, Buffer value) throws IOException { super.onResponseHeader(name,value); if (HttpHeaders.CACHE.getOrdinal(name)==HttpHeaders.SET_COOKIE_ORDINAL) { String cname=null; String cvalue=null; QuotedStringTokenizer tok=new QuotedStringTokenizer(value.toString(),"=;",false,false); tok.setSingle(false); if (tok.hasMoreElements()) cname=tok.nextToken(); if (tok.hasMoreElements()) cvalue=tok.nextToken(); Cookie cookie=new Cookie(cname,cvalue); while (tok.hasMoreTokens()) { String token=tok.nextToken(); if ("Version".equalsIgnoreCase(token)) cookie.setVersion(Integer.parseInt(tok.nextToken())); else if ("Comment".equalsIgnoreCase(token)) cookie.setComment(tok.nextToken()); else if ("Path".equalsIgnoreCase(token)) cookie.setPath(tok.nextToken()); else if ("Domain".equalsIgnoreCase(token)) cookie.setDomain(tok.nextToken()); else if ("Expires".equalsIgnoreCase(token)) { tok.nextToken(); // TODO } else if ("Max-Age".equalsIgnoreCase(token)) { tok.nextToken(); // TODO } else if ("Secure".equalsIgnoreCase(token)) cookie.setSecure(true); } BayeuxClient.this.setCookie(cookie); } } /* ------------------------------------------------------------ */ protected void onResponseComplete() throws IOException { super.onResponseComplete(); if (getResponseStatus()==200) { String content = getResponseContent(); if (content==null || content.length()==0) throw new IllegalStateException(); _responses=parse(content); } } /* ------------------------------------------------------------ */ protected void onExpire() { super.onExpire(); } /* ------------------------------------------------------------ */ protected void onConnectionFailed(Throwable ex) { super.onConnectionFailed(ex); if (++_connectFailures<5) { try { _client.send(this); } catch (IOException e) { Log.warn(e); } } } /* ------------------------------------------------------------ */ protected void onException(Throwable ex) { super.onException(ex); } } /* ------------------------------------------------------------ */ /** The Bayeux handshake exchange. * Negotiates a client Id and initializes the protocol. * */ private class Handshake extends Exchange { final static String __HANDSHAKE="[{"+"\"channel\":\"/meta/handshake\","+"\"version\":\"0.9\","+"\"minimumVersion\":\"0.9\""+"}]"; Handshake() { super("handshake"); setMessage(__HANDSHAKE); try { customize(this); _client.send(this); } catch (IOException e) { Log.warn(e); } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see org.mortbay.jetty.client.HttpExchange#onException(java.lang.Throwable) */ protected void onException(Throwable ex) { Log.warn("Handshake:"+ex); Log.debug(ex); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete() */ protected void onResponseComplete() throws IOException { super.onResponseComplete(); if (getResponseStatus()==200&&_responses!=null&&_responses.length>0) { Map<?,?> response=(Map<?,?>)_responses[0]; Boolean successful=(Boolean)response.get(Bayeux.SUCCESSFUL_FIELD); if (successful!=null&&successful.booleanValue()) { _clientId=(String)response.get(Bayeux.CLIENT_FIELD); _pull=new Connect(); } else throw new IOException("Handshake failed:"+_responses[0]); } else { throw new IOException("Handshake failed: "+getResponseStatus()); } } } /* ------------------------------------------------------------ */ /** The Bayeux Connect exchange. * Connect exchanges implement the long poll for Bayeux. */ private class Connect extends Exchange { Connect() { super("connect"); String connect="{"+"\"channel\":\"/meta/connect\","+"\"clientId\":\""+_clientId+"\","+"\"connectionType\":\"long-polling\""+"}"; setMessage(connect); try { customize(this); _client.send(this); } catch (IOException e) { Log.warn(e); } } protected void onResponseComplete() throws IOException { super.onResponseComplete(); if (getResponseStatus()==200&&_responses!=null&&_responses.length>0) { try { startBatch(); for (int i=0; i<_responses.length; i++) { Message msg=(Message)_responses[i]; if (Bayeux.META_CONNECT.equals(msg.get(Bayeux.CHANNEL_FIELD))) { Boolean successful=(Boolean)msg.get(Bayeux.SUCCESSFUL_FIELD); if (successful!=null&&successful.booleanValue()) { if (!_initialized) { _initialized=true; synchronized (_outQ) { if (_outQ.size()>0) _push=new Publish(); } } _pull=new Connect(); } else throw new IOException("Connect failed:"+_responses[0]); } deliver(null,msg); } } finally { endBatch(); } } else { throw new IOException("Connect failed: "+getResponseStatus()); } } } /* ------------------------------------------------------------ */ /** * Publish message exchange. * Sends messages to bayeux server and handles any messages received as a result. */ private class Publish extends Exchange { Publish() { super("publish"); synchronized (_outQ) { if (_outQ.size()==0) return; setMessages(_outQ); _outQ.clear(); } try { customize(this); _client.send(this); } catch (IOException e) { Log.warn(e); } } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see org.mortbay.cometd.client.BayeuxClient.Exchange#onResponseComplete() */ protected void onResponseComplete() throws IOException { super.onResponseComplete(); try { synchronized (_outQ) { startBatch(); _push=null; } if (getResponseStatus()==200&&_responses!=null&&_responses.length>0) { for (int i=0; i<_responses.length; i++) { Message msg=(Message)_responses[i]; deliver(null,msg); } } else { throw new IOException("Reconnect failed: "+getResponseStatus()); } } finally { endBatch(); } } } public void addListener(ClientListener listener) { synchronized(_inQ) { if (listener instanceof MessageListener) { if (_mListeners==null) _mListeners=new ArrayList<MessageListener>(); _mListeners.add((MessageListener)listener); } if (listener instanceof RemoveListener) { if (_rListeners==null) _rListeners=new ArrayList<RemoveListener>(); _rListeners.add((RemoveListener)listener); } } } public void removeListener(ClientListener listener) { synchronized(_inQ) { if (listener instanceof MessageListener) { if (_mListeners!=null) _mListeners.remove((MessageListener)listener); } if (listener instanceof RemoveListener) { if (_rListeners!=null) _rListeners.remove((RemoveListener)listener); } } } public int getMaxQueue() { return -1; } public Queue<Message> getQueue() { return _inQ; } public void setMaxQueue(int max) { if( max!=-1) throw new UnsupportedOperationException(); } }