/*
* Copyright 2009 Red Hat, Inc.
*
* Red Hat licenses this file to you 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.jboss.netty.handler.codec.bayeux;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.logging.InternalLogger;
import org.jboss.netty.logging.InternalLoggerFactory;
/**
* A Bayeux connection is the same as the Bayeux <a href="http://svn.cometd.org/trunk/bayeux/bayeux.html#toc_19">Channel</a> in protocol document.
*
* It's a connection between a client and a server, which can map many HTTP
* connections between the two. And in another hand, it also provides user APIs
* to develop Bayeux applications.
*
* @author daijun
*/
public class BayeuxConnection {
private static final InternalLogger logger =
InternalLoggerFactory.getInstance(BayeuxEncoder.class.getName());
private String clientId;
private TYPE connectionType;
private String jsonp;
private String connectionId;//Currently it's value equals clientId's and isn't used.
private STATE state;
private Channel channel;
private String id;//Message id of a connection
private boolean isCommented = false;//Wrap response JSON string with comment
private final LinkedList<BayeuxMessage> upstreamQueue = new LinkedList<BayeuxMessage>();//Receiving queue
private final LinkedList<BayeuxMessage> downstreamQueue = new LinkedList<BayeuxMessage>();//Sending queue
private final LinkedList<String> subscriptions = new LinkedList<String>();//Subscriptions, which are listenning to
private String requestedUri;
private String requestedHost;
private SocketAddress clientAddress;
private SocketAddress serverAddress;
public enum TYPE {
LONG_POLLING,
LONG_POLLING_JSON_ENCODED,
CALLBACK_POLLING,
IFRAME,
FLASH
}
public enum STATE {
INITIAL,
HANDSHAKED,
CONNECTED,
DISCONNECTED
}
public enum ERROR {
UNKNOWN_ERROR,
NO_CLIENT_ID,
UNKNOWN_CLIENT_ID,
UNKNOWN_CHANNEL,
DENIED_SUBSCRIPTION,
UNSUPPORTED_CONNECTION_TYPES,
UNSUPPORTED_VERSION,
REPEAT_SUBSCRIBE,
CONN_LIMIT_EXCEEDED
}
/**
* Initialize a BayeuxConnection with required properties.
*
* @param channel
*/
public BayeuxConnection() {
this.state = STATE.INITIAL;
}
/**
* Put a received Bayeux message to connection's upstream queue.
*
* @param bayeux
*/
public void putToUpstream(BayeuxMessage bayeux) {
upstreamQueue.add(bayeux);
}
/**
* Put list of Bayeux request messages to connection's upstream queue
*
* @param list
*/
public void putToUpstream(List<BayeuxMessage> list) {
for (BayeuxMessage bayeux : list) {
putToUpstream(bayeux);
}
}
/**
* Poll the first Bayeux request message from upstream queue, if queue is
* empty returns null.
*
* @return
*/
public BayeuxMessage getFromUpstream() {
return upstreamQueue.pollFirst();
}
/**
* If connection's downstream queue is not empty, write out all the messages
* in it to client and clear it.
*/
public synchronized void flush() {
String response = null;
if (!downstreamQueue.isEmpty()) {
response = JSONParser.toJSON(downstreamQueue);
if (isCommented) {
response = "/*" + response + "*/";
}
if (jsonp != null && jsonp.length() > 0) {
response = jsonp + "(" + response + ")";
}
}
if (response != null && response.length() > 0 && channel.isWritable()) {
ChannelFuture future = channel.write(response);
future.addListener(ChannelFutureListener.CLOSE);
downstreamQueue.clear();
}
}
/**
* Send a Bayeux message to connection's downstream queue, but it's
* not sent to client immediatly. It with other messages in downstream queue
* will be flush out, when flush() method is called for the next time.
*
* @param bayeux
*/
public void putToDownstream(BayeuxMessage bayeux) {
if (bayeux != null) {
downstreamQueue.add(bayeux);
}
}
/**
* Send Bayeux messages list to connection's downstream queue and will be
* sent out later like sendToQueue(BayeuxMessage bayeux).
*
* @param bayeux
*/
public void putToDownstream(List<BayeuxMessage> bayeuxes) {
for (BayeuxMessage bayeux : bayeuxes) {
putToDownstream(bayeux);
}
}
/**
* Send a Bayeux message to client immediatly.
*
* @param bayeux
*/
public void send(BayeuxMessage bayeux) {
putToDownstream(bayeux);
flush();
}
/**
* Send Bayeux messages list to client immediatly.
*
* @param bayeuxes
*/
public void send(List<BayeuxMessage> bayeuxes) {
for (BayeuxMessage bayeux : bayeuxes) {
putToDownstream(bayeux);
}
flush();
}
/**
* Send a normal string immediatly to client.
*
* @param response
*/
public void send(String response) {
if (channel.isWritable()) {
ChannelFuture future = channel.write(response);
future.addListener(ChannelFutureListener.CLOSE);
}
}
/**
* Clear upstream and downstream queue both.
*/
public void clear() {
upstreamQueue.clear();
downstreamQueue.clear();
}
/**
* Close connection.
*/
public void close() {
channel.close();
}
/**
* Handshake protocol version and connection type with server
*
* @param handshakeRequest
*/
public void handshake(HandshakeRequest handshakeRequest) {
BayeuxExt ext = handshakeRequest.getExt();
if (ext != null && ext.contains("json-comment-filtered")) {
this.isCommented = (Boolean) ext.get("json-comment-filtered");
}
HandshakeResponse handshakeResponse = new HandshakeResponse(handshakeRequest);
handshakeResponse.setClientId(this.clientId);
//Handshake connection type
TYPE[] clientSupportedConnectTypeList = handshakeRequest.getSupportedConnectionTypes();
List<TYPE> matchedConnectTypeList = new ArrayList<TYPE>();
Map<TYPE, TYPE> serverSupportedConnectTypeList = new HashMap();
serverSupportedConnectTypeList.put(TYPE.LONG_POLLING, TYPE.LONG_POLLING);
serverSupportedConnectTypeList.put(TYPE.CALLBACK_POLLING, TYPE.CALLBACK_POLLING);
for (int i = 0; i < clientSupportedConnectTypeList.length; i++) {
if (serverSupportedConnectTypeList.get(clientSupportedConnectTypeList[i]) != null) {
matchedConnectTypeList.add(clientSupportedConnectTypeList[i]);
}
}
if (matchedConnectTypeList.isEmpty()) {
handshakeResponse.setSuccessful(false);
handshakeResponse.setError(getValueOfError(ERROR.UNSUPPORTED_CONNECTION_TYPES, JSONParser.toJSON(clientSupportedConnectTypeList)));
handshakeResponse.setSupportedConnectionTypes((new ArrayList<TYPE>(serverSupportedConnectTypeList.values())).toArray(new TYPE[0]));
putToDownstream(handshakeResponse);
BayeuxRouter.getInstance().removeConnection(this);
return;
} else {
handshakeResponse.setSuccessful(true);
handshakeResponse.setSupportedConnectionTypes(matchedConnectTypeList.toArray(new TYPE[0]));
}
//Handshake Bayeux version
String clientMinimumVersion = handshakeRequest.getMinimumVersion();
String clientVersion = handshakeRequest.getVersion();
String serverMinimumVersion = "1.0beta";
String serverVersion = "1.0beta";
if (serverMinimumVersion.equalsIgnoreCase(clientMinimumVersion)) {
handshakeResponse.setMinimumVersion(serverMinimumVersion);
handshakeResponse.setSuccessful(true);
} else if (compareVersion(serverMinimumVersion, clientMinimumVersion)) {
handshakeResponse.setMinimumVersion(serverMinimumVersion);
handshakeResponse.setSuccessful(!compareVersion(serverMinimumVersion, clientVersion));
} else {
handshakeResponse.setMinimumVersion(clientMinimumVersion);
handshakeResponse.setSuccessful(!compareVersion(clientMinimumVersion, serverVersion));
}
if (handshakeResponse.isSuccessful()) {
handshakeResponse.setVersion(compareVersion(serverVersion, clientVersion) ? clientVersion : serverVersion);
this.state = STATE.HANDSHAKED;
} else {
handshakeResponse.setMinimumVersion(serverMinimumVersion);
handshakeResponse.setVersion(serverVersion);
handshakeResponse.setError(getValueOfError(ERROR.UNSUPPORTED_VERSION, clientMinimumVersion + "," + clientVersion));
BayeuxRouter.getInstance().removeConnection(this);
}
putToDownstream(handshakeResponse);
}
/**
* Connect to server.
*
* @param connectRequest
*/
public void connect(ConnectRequest connectRequest) {
if (this.state == STATE.HANDSHAKED) {
this.connectionType = connectRequest.getConnectionType();
this.state = STATE.CONNECTED;
ConnectResponse connectResponse = new ConnectResponse(connectRequest);
connectResponse.setSuccessful(true);
putToDownstream(connectResponse);
} else if (this.state == STATE.CONNECTED) {
return;
} else {
ConnectResponse connectResponse = new ConnectResponse(connectRequest);
connectResponse.setSuccessful(false);
connectResponse.setAdvice(new BayeuxAdvice("handshake", 0, false));
connectResponse.setError(getValueOfError(ERROR.UNKNOWN_ERROR, null));
putToDownstream(connectResponse);
this.state = STATE.DISCONNECTED;
BayeuxRouter.getInstance().removeConnection(this);
}
}
/**
* Disconnect from server.
*
* @param disconnectRequest
*/
public void disconnect(DisconnectRequest disconnectRequest) {
boolean successful = BayeuxRouter.getInstance().removeConnection(this);
subscriptions.clear();
DisconnectResponse disconnectResponse = new DisconnectResponse(disconnectRequest);
disconnectResponse.setSuccessful(successful);
if (!successful) {
disconnectResponse.setError(getValueOfError(ERROR.UNKNOWN_CLIENT_ID, disconnectResponse.getClientId()));
}
putToDownstream(disconnectResponse);
}
/**
* Subscribe to channel.
*
* @param subscribeRequest
*/
public void subscribe(SubscribeRequest subscribeRequest) {
String subscription = subscribeRequest.getSubscription();
boolean successful = BayeuxRouter.getInstance().addListener(subscription, this);
SubscribeResponse subscribeResponse = new SubscribeResponse(subscribeRequest);
subscribeResponse.setSuccessful(successful);
if (successful) {
subscriptions.add(subscription);
} else {
subscribeResponse.setAdvice(new BayeuxAdvice("retry", 0, false));
subscribeResponse.setError(getValueOfError(ERROR.REPEAT_SUBSCRIBE, subscribeRequest.getClientId() + "," + subscribeRequest.getSubscription()));
}
putToDownstream(subscribeResponse);
}
/**
* Unsubscribe from channel.
*
* @param unsubscribeRequest
*/
public void unsubscribe(UnsubscribeRequest unsubscribeRequest) {
String subscription = unsubscribeRequest.getSubscription();
boolean successful = BayeuxRouter.getInstance().removeListener(subscription, this);
UnsubscribeResponse unsubscribeResponse = new UnsubscribeResponse(unsubscribeRequest);
unsubscribeResponse.setSuccessful(successful);
if (successful) {
subscriptions.remove(subscription);
} else {
unsubscribeResponse.setAdvice(new BayeuxAdvice("retry", 0, false));
unsubscribeResponse.setError(getValueOfError(ERROR.UNKNOWN_CHANNEL, unsubscribeRequest.getClientId() + "," + unsubscribeRequest.getSubscription()));
}
putToDownstream(unsubscribeResponse);
}
/**
* Publish data to a channel
*
* @param publishRequest
*/
public void publish(PublishRequest publishRequest) {
DeliverEvent deliver=new DeliverEvent(publishRequest);
deliver.setClientId(this.clientId);
deliver.setId(this.id);
boolean successful = BayeuxRouter.getInstance().publish(this, deliver);
PublishResponse publishResponse = new PublishResponse(publishRequest);
publishResponse.setSuccessful(successful);
if (!successful) {
publishResponse.setError(getValueOfError(ERROR.UNKNOWN_CHANNEL, publishRequest.getClientId() + "," + publishRequest.getChannel()));
}
putToDownstream(publishResponse);
}
/**
* Compare two versions in format of String, returns true if version1 > version, and verse.
*
* @param version1
* @param version2
* @return
*/
private boolean compareVersion(String version1, String version2) {
for (int i = 0; i < Math.min(version1.length(), version2.length()); i++) {
if (version1.charAt(i) != version2.charAt(i)) {
return version1.charAt(i) > version2.charAt(i);
}
}
return false;
}
public static TYPE getTypeOfValue(String connection_type) {
if ("long-polling".equalsIgnoreCase(connection_type)) {
return TYPE.LONG_POLLING;
} else if ("long-polling-json-encoded".equalsIgnoreCase(connection_type)) {
return TYPE.LONG_POLLING_JSON_ENCODED;
} else if ("callback-polling".equalsIgnoreCase(connection_type)) {
return TYPE.CALLBACK_POLLING;
} else if ("iframe".equalsIgnoreCase(connection_type)) {
return TYPE.IFRAME;
} else if ("flash".equalsIgnoreCase(connection_type)) {
return TYPE.FLASH;
} else {
return null;
}
}
public static String getValueOfType(TYPE connectionType) {
switch (connectionType) {
case LONG_POLLING:
return "long-polling";
case LONG_POLLING_JSON_ENCODED:
return "long-polling-json-encoded";
case CALLBACK_POLLING:
return "callback-polling";
case IFRAME:
return "iframe";
case FLASH:
return "flash";
default:
return "";
}
}
public static String getValueOfError(ERROR error, String msg) {
switch (error) {
case NO_CLIENT_ID:
return "401::No Client ID";
case UNKNOWN_CLIENT_ID:
return "402:" + msg + ":Unknown Client ID";
case DENIED_SUBSCRIPTION:
return "403:" + msg + ":Subscription denied";
case UNKNOWN_CHANNEL:
return "404:" + msg + ":Unknown Channel";
case UNSUPPORTED_CONNECTION_TYPES:
return "405:" + msg + ":Unsupported Connection Types";
case UNSUPPORTED_VERSION:
return "406:" + msg + ":Unsupported version";
case REPEAT_SUBSCRIBE:
return "406:" + msg + ":Repeat subscribe";
case CONN_LIMIT_EXCEEDED:
return "407::Exceed connections limit "+msg;
default:
return "400::Unknown Error";
}
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getConnectionId() {
return connectionId;
}
public void setConnectionId(String connectionId) {
this.connectionId = connectionId;
}
public TYPE getConnectionType() {
return connectionType;
}
public void setConnectionType(TYPE connectionType) {
this.connectionType = connectionType;
}
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public STATE getState() {
return state;
}
public void setState(STATE state) {
this.state = state;
}
public String getJsonp() {
return jsonp;
}
public void setJsonp(String jsonp) {
this.jsonp = jsonp;
}
public LinkedList<BayeuxMessage> getDownstreamQueue() {
return downstreamQueue;
}
public LinkedList<String> getSubscriptions() {
return subscriptions;
}
public LinkedList<BayeuxMessage> getUpstreamQueue() {
return upstreamQueue;
}
public boolean isIsCommented() {
return isCommented;
}
public void setIsCommented(boolean isCommented) {
this.isCommented = isCommented;
}
public String getRequestedHost() {
return requestedHost;
}
public void setRequestedHost(String requestedHost) {
this.requestedHost = requestedHost;
}
public String getRequestedUri() {
return requestedUri;
}
public void setRequestedUri(String requestedUri) {
this.requestedUri = requestedUri;
}
public SocketAddress getClientAddress() {
return clientAddress;
}
public void setClientAddress(SocketAddress clientAddress) {
this.clientAddress = clientAddress;
}
public SocketAddress getServerAddress() {
return serverAddress;
}
public void setServerAddress(SocketAddress serverAddress) {
this.serverAddress = serverAddress;
}
}