org.cometd.oort.Oort Maven / Gradle / Ivy
package org.cometd.oort;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.BayeuxServer.Extension;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerMessage.Mutable;
import org.cometd.bayeux.server.ServerSession;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.log.Log;
/* ------------------------------------------------------------ */
/**
* Oort cluster of cometd servers.
*
* This class maintains a collection of {@link OortComet} instances to each
* comet server identified by calls to {@link #observeComet(String)}. The Oort
* instance is created and configured by {@link OortServlet}.
*
* The key configuration parameter that must be set is the Oort URL, which is
* full public URL to the cometd servlet, eg. http://myserver:8080/context/cometd
* See {@link OortServlet} for more configuration detail.
* @author gregw
*
*/
public class Oort
{
public final static String OORT_URL = "oort.url";
public final static String OORT_CLOUD = "oort.cloud";
public final static String OORT_CHANNELS = "oort.channels";
public final static String OORT_ATTRIBUTE = "org.cometd.oort.Oort";
final protected String _url;
final protected String _secret;
final protected BayeuxServer _bayeux;
final protected HttpClient _httpClient;
final protected Random _random=new SecureRandom();
final protected LocalSession _oortSession;
final protected Map _knownCommets = new ConcurrentHashMap();
final protected Set _channels = Collections.newSetFromMap(new ConcurrentHashMap());
/* ------------------------------------------------------------ */
Oort(String id,BayeuxServer bayeux)
{
_url=id;
_bayeux=bayeux;
_secret=Long.toHexString(_random.nextLong());
_httpClient=new HttpClient();
_oortSession=_bayeux.newLocalSession("oort");
bayeux.addExtension(new OortExtension());
try
{
_httpClient.start();
_oortSession.handshake();
_oortSession.getChannel("/oort/cloud").subscribe(new RootOortClientListener());
}
catch(Exception e)
{
throw new RuntimeException(e);
}
}
/* ------------------------------------------------------------ */
public BayeuxServer getBayeux()
{
return _bayeux;
}
/* ------------------------------------------------------------ */
/**
* @return The oublic absolute URL of the Oort cometd server.
*/
public String getURL()
{
return _url;
}
/* ------------------------------------------------------------ */
public String getSecret()
{
return _secret;
}
/* ------------------------------------------------------------ */
/**
* Observe an Oort Comet server.
*
* The the comet server is not already observed, start a {@link OortComet}
* instance for it.
*
* @param cometUrl
* @return The {@link OortComet} instance for the comet server.
*/
public OortComet observeComet(String cometUrl)
{
synchronized (this)
{
if (_url.equals(cometUrl))
return null;
OortComet comet = _knownCommets.get(cometUrl);
if (comet==null)
{
try
{
comet = new OortComet(this,cometUrl);
_knownCommets.put(cometUrl,comet);
comet.handshake();
}
catch(Exception e)
{
throw new IllegalStateException(e);
}
}
return comet;
}
}
/* ------------------------------------------------------------ */
/**
* Pass observed comets.
*
* Called when another comet server publishes it's list of
* known comets to the /oort/cloud channel. If the list contains
* any unknown commets, then {@link #observeComet(String)} is
* called for each.
* @param comets
*/
void observedComets(Set comets)
{
synchronized (this)
{
Set known=getKnownComets();
for (String comet : comets)
if (!_url.equals(comet))
observeComet(comet);
known=getKnownComets();
if (!comets.containsAll(known))
_bayeux.getChannel("/oort/cloud").publish(_oortSession,known,null);
}
}
/* ------------------------------------------------------------ */
/**
* @return The set of known Oort comet servers URLs.
*/
public Set getKnownComets()
{
synchronized (this)
{
Set comets = new HashSet(_knownCommets.keySet());
comets.add(_url);
return comets;
}
}
/* ------------------------------------------------------------ */
/**
* Observer a channel.
*
* Once observed, all {@link OortComet} instances subscribe
* to the channel and will repeat any messages published to
* the local channel (with loop prevention), so that the
* messages are distributed to all Oort comet servers.
* @param channelId
*/
public void observeChannel(String channelId)
{
if (_channels.add(channelId))
{
for (OortComet comet : _knownCommets.values())
comet.subscribe(channelId);
}
}
/* ------------------------------------------------------------ */
public boolean isOort(ServerSession session)
{
LocalSession local=session.getLocalSession();
return local==_oortSession;
}
/* ------------------------------------------------------------ */
public boolean isOort(LocalSession session)
{
return session==_oortSession;
}
/* ------------------------------------------------------------ */
public String toString()
{
return _url;
}
/* ------------------------------------------------------------ */
/**
* Called to register the details of a successful handshake with an
* Oort comet. A {@link RemoteOortClientListener} instance is added to
* the local Oort client instance.
* @param oortUrl
* @param oortSecret
* @param clientId
*/
protected void oortHandshook(String oortUrl,String oortSecret,String clientId)
{
Log.info(this+": "+clientId+" is oort "+oortUrl);
if (!_knownCommets.containsKey(oortUrl))
observeComet(oortUrl);
ServerSession session = _bayeux.getSession(clientId);
session.addExtension(new RemoteOortClientExtension());
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/**
* Extension to detect incoming handshake from other Oort servers
* and to call {@link Oort#oortHandshook(String, String, String)}.
*
*/
protected class OortExtension implements Extension
{
@Override
public boolean rcv(ServerSession from, Mutable message)
{
return true;
}
@Override
public boolean rcvMeta(ServerSession from, Mutable message)
{
return true;
}
@Override
public boolean send(ServerSession from, ServerSession to, Mutable message)
{
return true;
}
@Override
public boolean sendMeta(ServerSession to, Mutable message)
{
if (message.getChannel().equals(Channel.META_HANDSHAKE) && message.isSuccessful())
{
Message rcv = message.getAssociated();
if (Log.isDebugEnabled()) Log.debug(_url+" --> "+rcv);
Map rcvExt = rcv.getExt();
if (rcvExt!=null)
{
Map oort = (Map)rcvExt.get("oort");
if (oort!=null)
{
String cometUrl = (String)oort.get("comet");
String oortUrl = (String)oort.get("oort");
if (getURL().equals(cometUrl))
{
String oortSecret = (String)oort.get("oortSecret");
oortHandshook(oortUrl,oortSecret,message.getClientId());
Object ext=message.get("ext");
Map sndExt = (Map)((ext instanceof JSON.Literal)?JSON.parse(ext.toString()):ext);
if (sndExt==null)
sndExt = new HashMap();
oort.put("cometSecret",getSecret());
sndExt.put("oort",oort);
message.put("ext",sndExt);
}
}
}
if (Log.isDebugEnabled()) Log.debug(_url+" <-- "+message);
}
return true;
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/**
* An Extension installed on sessions for remote Oort servers
* that prevents publish loops.
*/
protected class RemoteOortClientExtension implements org.cometd.bayeux.server.ServerSession.Extension
{
@Override
public boolean rcv(ServerSession session, Mutable message)
{
return true;
}
@Override
public boolean rcvMeta(ServerSession session, Mutable message)
{
return true;
}
@Override
public ServerMessage send(ServerSession to, ServerMessage message)
{
// avoid loops
boolean send = !isOort(to) || message.getChannel().startsWith("/oort/");
return send?message:null;
}
@Override
public boolean sendMeta(ServerSession session, Mutable message)
{
return true;
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/**
* MessageListener that handles publishes to /oort/cloud
*/
protected class RootOortClientListener implements ClientSessionChannel.MessageListener
{
@Override
public void onMessage(ClientSessionChannel channel, Message msg)
{
String channelId = msg.getChannel();
Object data=msg.getData();
if (data instanceof Object[])
{
Object[] array = (Object[])msg.getData();
Set comets = new HashSet();
for (Object o:array)
comets.add(o.toString());
observedComets(comets);
}
}
}
}