Package org.cometd.oort

Source Code of org.cometd.oort.Oort

/*
* Copyright (c) 2008-2014 the original author or authors.
*
* 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.cometd.oort;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;

import org.cometd.bayeux.Channel;
import org.cometd.bayeux.ChannelId;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSession;
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.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerMessage.Mutable;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.client.ext.AckExtension;
import org.cometd.client.transport.ClientTransport;
import org.cometd.client.transport.LongPollingTransport;
import org.cometd.common.HashMapMessage;
import org.cometd.common.JSONContext;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.cometd.server.ext.AcknowledgedMessagesExtension;
import org.cometd.websocket.client.WebSocketTransport;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* <p>Oort is the cluster manager that links one CometD server to a set of other CometD servers.</p>
* <p>The Oort instance is created and configured by either {@link OortMulticastConfigServlet} or
* {@link OortStaticConfigServlet}.</p>
* <p>This class maintains a collection of {@link OortComet} instances to each
* CometD server, created by calls to {@link #observeComet(String)}.</p>
* <p>The key configuration parameter is the Oort URL, which is
* full public URL of the CometD servlet to which the Oort instance is bound,
* for example: <code>http://myserver:8080/context/cometd</code>.</p>
* <p>Oort instances can be configured with a shared {@link #setSecret(String) secret}, which allows
* the Oort instance to distinguish handshakes coming from remote clients from handshakes coming from
* other Oort comets: the firsts may be subject to a stricter authentication policy than the seconds.</p>
*
* @see OortMulticastConfigServlet
* @see OortStaticConfigServlet
*/
@ManagedObject("CometD cloud node")
public class Oort extends ContainerLifeCycle
{
    public final static String OORT_ATTRIBUTE = Oort.class.getName();
    public static final String EXT_OORT_FIELD = "org.cometd.oort";
    public static final String EXT_OORT_URL_FIELD = "oortURL";
    public static final String EXT_OORT_ID_FIELD = "oortId";
    public static final String EXT_OORT_SECRET_FIELD = "oortSecret";
    public static final String EXT_COMET_URL_FIELD = "cometURL";
    public static final String EXT_OORT_ALIAS_URL_FIELD = "oortAliasURL";
    public static final String OORT_CLOUD_CHANNEL = "/oort/cloud";
    public static final String OORT_SERVICE_CHANNEL = "/service/oort";
    private static final String COMET_URL_ATTRIBUTE = EXT_OORT_FIELD + "." + EXT_COMET_URL_FIELD;

    private final ConcurrentMap<String, OortComet> _pendingComets = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, ClientCometInfo> _clientComets = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, ServerCometInfo> _serverComets = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, Boolean> _channels = new ConcurrentHashMap<>();
    private final CopyOnWriteArrayList<CometListener> _cometListeners = new CopyOnWriteArrayList<>();
    private final Extension _oortExtension = new OortExtension();
    private final ServerChannel.MessageListener _cloudListener = new CloudListener();
    private final ServerChannel.MessageListener _joinListener = new JoinListener();
    private final List<ClientTransport.Factory> _transportFactories = new ArrayList<>();
    private final BayeuxServer _bayeux;
    private final String _url;
    private final String _id;
    private final Logger _logger;
    private final LocalSession _oortSession;
    private ScheduledExecutorService _scheduler;
    private String _secret;
    private boolean _ackExtensionEnabled;
    private Extension _ackExtension;
    private JSONContext.Client _jsonContext;

    public Oort(BayeuxServer bayeux, String url)
    {
        _bayeux = bayeux;
        _url = url;
        _id = UUID.randomUUID().toString();

        _logger = LoggerFactory.getLogger(getClass().getName() + "." + replacePunctuation(_url, '_'));

        _oortSession = bayeux.newLocalSession("oort");
        _secret = Long.toHexString(new SecureRandom().nextLong());
    }

    @Override
    protected void doStart() throws Exception
    {
        ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1);
        scheduler.setRemoveOnCancelPolicy(true);
        _scheduler = scheduler;

        if (_transportFactories.isEmpty())
        {
            _transportFactories.add(new WebSocketTransport.Factory());
            _transportFactories.add(new LongPollingTransport.Factory(new HttpClient()));
        }
        for (ClientTransport.Factory factory : _transportFactories)
            addBean(factory);

        super.doStart();

        if (isAckExtensionEnabled())
        {
            boolean present = false;
            for (Extension extension : _bayeux.getExtensions())
            {
                if (extension instanceof AcknowledgedMessagesExtension)
                {
                    present = true;
                    break;
                }
            }
            if (!present)
                _bayeux.addExtension(_ackExtension = new AcknowledgedMessagesExtension());
        }

        _bayeux.addExtension(_oortExtension);

        ServerChannel oortCloudChannel = _bayeux.createChannelIfAbsent(OORT_CLOUD_CHANNEL).getReference();
        oortCloudChannel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
        oortCloudChannel.addListener(_cloudListener);

        ServerChannel oortServiceChannel = _bayeux.createChannelIfAbsent(OORT_SERVICE_CHANNEL).getReference();
        oortServiceChannel.addListener(_joinListener);

        _oortSession.handshake();
    }

    @Override
    protected void doStop() throws Exception
    {
        _oortSession.disconnect();

        for (OortComet comet : _pendingComets.values())
            comet.disconnect(1000);
        _pendingComets.clear();

        for (ClientCometInfo cometInfo : _clientComets.values())
            cometInfo.comet.disconnect(1000);
        _clientComets.clear();

        _serverComets.clear();
        _channels.clear();

        ServerChannel channel = _bayeux.getChannel(OORT_SERVICE_CHANNEL);
        if (channel != null)
            channel.removeListener(_joinListener);

        channel = _bayeux.getChannel(OORT_CLOUD_CHANNEL);
        if (channel != null)
        {
            channel.removeListener(_cloudListener);
            channel.removeAuthorizer(GrantAuthorizer.GRANT_ALL);
        }

        Extension ackExtension = _ackExtension;
        _ackExtension = null;
        if (ackExtension != null)
            _bayeux.removeExtension(ackExtension);

        _bayeux.removeExtension(_oortExtension);

        _scheduler.shutdownNow();

        super.doStop();
    }

    @ManagedAttribute(value = "The BayeuxServer of this Oort", readonly = true)
    public BayeuxServer getBayeuxServer()
    {
        return _bayeux;
    }

    /**
     * @return the public absolute URL of the Oort CometD server
     */
    @ManagedAttribute(value = "The URL of this Oort", readonly = true)
    public String getURL()
    {
        return _url;
    }

    @ManagedAttribute(value = "The unique ID of this Oort", readonly = true)
    public String getId()
    {
        return _id;
    }

    @ManagedAttribute("The secret of this Oort")
    public String getSecret()
    {
        return _secret;
    }

    public void setSecret(String secret)
    {
        this._secret = secret;
    }

    @ManagedAttribute("Whether the acknowledgement extension is enabled")
    public boolean isAckExtensionEnabled()
    {
        return _ackExtensionEnabled;
    }

    public void setAckExtensionEnabled(boolean value)
    {
        _ackExtensionEnabled = value;
    }

    public JSONContext.Client getJSONContextClient()
    {
        return _jsonContext;
    }

    public void setJSONContextClient(JSONContext.Client jsonContext)
    {
        _jsonContext = jsonContext;
    }

    public List<ClientTransport.Factory> getClientTransportFactories()
    {
        return _transportFactories;
    }

    /**
     * <p>Connects (if not already connected) and observes another Oort instance
     * (identified by the given URL) via a {@link OortComet} instance.</p>
     *
     * @param cometURL the Oort URL to observe
     * @return The {@link OortComet} instance associated to the Oort instance identified by the URL
     *         or null if the given Oort URL represent this Oort instance
     */
    public OortComet observeComet(String cometURL)
    {
        return observeComet(cometURL, null);
    }

    protected OortComet observeComet(String cometURL, String cometAliasURL)
    {
        if (_logger.isDebugEnabled())
            _logger.debug("Observing comet {}", cometURL);
        try
        {
            URI uri = new URI(cometURL);
            if (uri.getScheme() == null)
                throw new IllegalArgumentException("Missing protocol in comet URL " + cometURL);
            if (uri.getHost() == null)
                throw new IllegalArgumentException("Missing host in comet URL " + cometURL);
        }
        catch (URISyntaxException x)
        {
            throw new IllegalArgumentException(x);
        }

        if (_url.equals(cometURL))
            return null;

        OortComet comet = getComet(cometURL);
        if (comet != null)
        {
            if (_logger.isDebugEnabled())
                _logger.debug("Comet {} is already connected", cometURL);
            return comet;
        }

        comet = newOortComet(cometURL);
        configureOortComet(comet);
        OortComet existing = _pendingComets.putIfAbsent(cometURL, comet);
        if (existing != null)
        {
            if (_logger.isDebugEnabled())
                _logger.debug("Comet {} is already connecting", cometURL);
            return existing;
        }

        comet.getChannel(Channel.META_HANDSHAKE).addListener(new HandshakeListener(cometURL, comet));

        if (_logger.isDebugEnabled())
            _logger.debug("Connecting to comet {}", cometURL);

        String b64Secret = encodeSecret(getSecret());
        Message.Mutable fields = new HashMapMessage();
        Map<String, Object> ext = fields.getExt(true);
        Map<String, Object> oortExt = new HashMap<>(4);
        ext.put(EXT_OORT_FIELD, oortExt);
        oortExt.put(EXT_OORT_URL_FIELD, getURL());
        oortExt.put(EXT_OORT_ID_FIELD, getId());
        oortExt.put(EXT_OORT_SECRET_FIELD, b64Secret);
        oortExt.put(EXT_COMET_URL_FIELD, cometURL);
        if (cometAliasURL != null)
            oortExt.put(EXT_OORT_ALIAS_URL_FIELD, cometAliasURL);
        connectComet(comet, fields);
        return comet;
    }

    protected OortComet newOortComet(String cometURL)
    {
        Map<String, Object> options = new HashMap<>(2);
        JSONContext.Client jsonContext = getJSONContextClient();
        if (jsonContext != null)
            options.put(ClientTransport.JSON_CONTEXT_OPTION, jsonContext);
        String maxMessageSizeOption = WebSocketTransport.PREFIX + "." + WebSocketTransport.MAX_MESSAGE_SIZE_OPTION;
        Object option = _bayeux.getOption(maxMessageSizeOption);
        if (option != null)
        {
            long value = option instanceof Number ? ((Number)option).longValue() : Long.parseLong(option.toString());
            options.put(maxMessageSizeOption, value);
        }
        options.put(ClientTransport.SCHEDULER_OPTION, _scheduler);

        List<ClientTransport> transports = new ArrayList<>();
        for (ClientTransport.Factory factory : getClientTransportFactories())
            transports.add(factory.newClientTransport(cometURL, options));

        ClientTransport transport = transports.get(0);
        int size = transports.size();
        ClientTransport[] otherTransports = transports.subList(1, size).toArray(new ClientTransport[size - 1]);

        return new OortComet(this, cometURL, _scheduler, transport, otherTransports);
    }

    protected void configureOortComet(OortComet oortComet)
    {
        if (isAckExtensionEnabled())
        {
            boolean present = false;
            for (ClientSession.Extension extension : oortComet.getExtensions())
            {
                if (extension instanceof AckExtension)
                {
                    present = true;
                    break;
                }
            }
            if (!present)
                oortComet.addExtension(new AckExtension());
        }
    }

    protected String encodeSecret(String secret)
    {
        try
        {
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            return new String(B64Code.encode(digest.digest(secret.getBytes("UTF-8"))));
        }
        catch (Exception x)
        {
            throw new IllegalArgumentException(x);
        }
    }

    protected void connectComet(OortComet comet, Message.Mutable fields)
    {
        comet.handshake(fields);
    }

    public OortComet deobserveComet(String cometURL)
    {
        if (_url.equals(cometURL))
            return null;

        OortComet comet = _pendingComets.remove(cometURL);
        if (comet != null)
        {
            if (_logger.isDebugEnabled())
                _logger.debug("Disconnecting pending comet {}", cometURL);
            comet.disconnect();
        }

        Iterator<ClientCometInfo> cometInfos = _clientComets.values().iterator();
        while (cometInfos.hasNext())
        {
            ClientCometInfo cometInfo = cometInfos.next();
            if (cometInfo.matchesURL(cometURL))
            {
                if (_logger.isDebugEnabled())
                    _logger.debug("Disconnecting comet {}", cometURL);
                comet = cometInfo.getOortComet();
                comet.disconnect();
                cometInfos.remove();
                break;
            }
        }

        return comet;
    }

    /**
     * @return the set of known Oort comet servers URLs.
     */
    @ManagedAttribute(value = "URLs of known Oorts in the cluster", readonly = true)
    public Set<String> getKnownComets()
    {
        Set<String> result = new HashSet<>();
        for (ClientCometInfo cometInfo : _clientComets.values())
            result.add(cometInfo.getOortURL());
        return result;
    }

    /**
     * @param cometURL the URL of a Oort comet
     * @return the OortComet instance connected with the Oort comet with the given URL
     */
    public OortComet getComet(String cometURL)
    {
        for (ClientCometInfo cometInfo : _clientComets.values())
        {
            if (cometInfo.matchesURL(cometURL))
                return cometInfo.getOortComet();
        }
        return null;
    }

    /**
     *
     * @param cometURL the URL of a Oort comet
     * @return the OortComet instance connecting or connected with the Oort comet with the given URL
     */
    protected OortComet findComet(String cometURL)
    {
        OortComet result = getComet(cometURL);
        if (result == null)
            result = _pendingComets.get(cometURL);
        return result;
    }

    /**
     * <p>Observes the given channel, registering to receive messages from
     * the Oort comets connected to this Oort instance.</p>
     * <p>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.</p>
     *
     * @param channelName the channel to observe
     */
    @ManagedOperation(value = "Observes the given channel", impact = "ACTION")
    public void observeChannel(@Name(value = "channel", description = "The channel to observe") String channelName)
    {
        if (_logger.isDebugEnabled())
            _logger.debug("Observing channel {}", channelName);

        if (!ChannelId.isBroadcast(channelName))
            throw new IllegalArgumentException("Channel " + channelName + " cannot be observed because is not a broadcast channel");

        if (_channels.putIfAbsent(channelName, Boolean.TRUE) == null)
        {
            Set<String> observedChannels = getObservedChannels();
            for (ClientCometInfo cometInfo : _clientComets.values())
                cometInfo.getOortComet().subscribe(observedChannels);
        }
    }

    @ManagedOperation(value = "Deobserves the given channel", impact = "ACTION")
    public void deobserveChannel(@Name(value = "channel", description = "The channel to deobserve") String channelId)
    {
        if (_channels.remove(channelId) != null)
        {
            for (ClientCometInfo cometInfo : _clientComets.values())
                cometInfo.getOortComet().unsubscribe(channelId);
        }
    }

    /**
     * @param session the server session to test
     * @return whether the given server session is one of those created by the Oort internal working
     * @see #isOortHandshake(Message)
     */
    public boolean isOort(ServerSession session)
    {
        String id = session.getId();

        if (id.equals(_oortSession.getId()))
            return true;

        for (ServerCometInfo cometInfo : _serverComets.values())
        {
            if (cometInfo.getServerSession().getId().equals(session.getId()))
                return true;
        }

        return false;
    }

    /**
     * @param handshake the handshake message to test
     * @return whether the given handshake message is coming from another Oort comet
     * that has been configured with the same {@link #setSecret(String) secret}
     * @see #isOort(ServerSession)
     */
    public boolean isOortHandshake(Message handshake)
    {
        if (!Channel.META_HANDSHAKE.equals(handshake.getChannel()))
            return false;
        Map<String, Object> ext = handshake.getExt();
        if (ext == null)
            return false;
        Object oortExtObject = ext.get(EXT_OORT_FIELD);
        if (!(oortExtObject instanceof Map))
            return false;
        @SuppressWarnings("unchecked")
        Map<String, Object> oortExt = (Map<String, Object>)oortExtObject;
        String cometURL = (String)oortExt.get(EXT_COMET_URL_FIELD);
        if (!getURL().equals(cometURL))
            return false;
        String b64RemoteSecret = (String)oortExt.get(EXT_OORT_SECRET_FIELD);
        String b64LocalSecret = encodeSecret(getSecret());
        return b64LocalSecret.equals(b64RemoteSecret);
    }

    /**
     * @param oortURL the comet URL to check for connection
     * @return whether the given comet is connected to this comet
     */
    protected boolean isCometConnected(String oortURL)
    {
        for (ServerCometInfo serverCometInfo : _serverComets.values())
        {
            if (serverCometInfo.getOortURL().equals(oortURL))
                return true;
        }
        return false;
    }

    /**
     * <p>Called to register the details of a successful handshake from another Oort comet.</p>
     *
     * @param oortExt the remote Oort information
     * @param session the server session that represent the connection with the remote Oort comet
     * @return false if a connection from a remote Oort has already been established, true otherwise
     */
    protected boolean incomingCometHandshake(Map<String, Object> oortExt, ServerSession session)
    {
        String remoteOortURL = (String)oortExt.get(EXT_OORT_URL_FIELD);
        if (_logger.isDebugEnabled())
            _logger.debug("Incoming comet handshake from comet {} with {}", remoteOortURL, session);

        String remoteOortId = (String)oortExt.get(EXT_OORT_ID_FIELD);
        ServerCometInfo serverCometInfo = new ServerCometInfo(remoteOortId, remoteOortURL, session);
        ServerCometInfo existing = _serverComets.putIfAbsent(remoteOortId, serverCometInfo);
        if (existing != null)
        {
            if (_logger.isDebugEnabled())
                _logger.debug("Comet {} is already known with {}", remoteOortURL, existing.getServerSession());
            return false;
        }

        session.setAttribute(COMET_URL_ATTRIBUTE, remoteOortURL);

        // Be notified when the remote comet stops
        session.addListener(new OortCometDisconnectListener(remoteOortURL, remoteOortId));
        // Prevent loops in sending/receiving messages
        session.addListener(new OortCometLoopListener());

        return true;
    }

    /**
     * Registers the given listener to be notified of comet events.
     * @param listener the listener to add
     */
    public void addCometListener(CometListener listener)
    {
        _cometListeners.add(listener);
    }

    /**
     * Deregisters the given listener from being notified of comet events.
     * @param listener the listener to remove
     */
    public void removeCometListener(CometListener listener)
    {
        _cometListeners.remove(listener);
    }

    private void notifyCometJoined(String remoteURL)
    {
        CometListener.Event event = new CometListener.Event(this, remoteURL);
        for (CometListener cometListener : _cometListeners)
        {
            try
            {
                cometListener.cometJoined(event);
            }
            catch (Throwable x)
            {
                _logger.info("Exception while invoking listener " + cometListener, x);
            }
        }
    }

    private void notifyCometLeft(String remoteURL)
    {
        CometListener.Event event = new CometListener.Event(this, remoteURL);
        for (CometListener cometListener : _cometListeners)
        {
            try
            {
                cometListener.cometLeft(event);
            }
            catch (Throwable x)
            {
                _logger.info("Exception while invoking listener " + cometListener, x);
            }
        }
    }

    protected void joinComets(Message message)
    {
        Object data = message.getData();
        Object[] array = data instanceof List ? ((List)data).toArray() : (Object[])data;
        for (Object element : array)
            observeComet((String)element);
    }

    public Set<String> getObservedChannels()
    {
        return new HashSet<>(_channels.keySet());
    }

    /**
     * @return the oortSession
     */
    public LocalSession getOortSession()
    {
        return _oortSession;
    }

    protected static String replacePunctuation(String source, char replacement)
    {
        String replaced = source.replaceAll("[^\\p{Alnum}]", String.valueOf(replacement));
        // Compact multiple consecutive replacement chars
        return replaced.replaceAll("(" + replacement + ")\\1+", "$1");
    }

    @Override
    public void dump(Appendable out, String indent) throws IOException
    {
        super.dump(out, indent);
        final String eol = System.getProperty("line.separator");
        Dumpable comets = new Dumpable()
        {
            public String dump()
            {
                return null;
            }

            public void dump(Appendable out, String indent) throws IOException
            {
                Set<String> knownComets = getKnownComets();
                out.append("Connected comets: ").append(String.valueOf(knownComets.size())).append(eol);
                ContainerLifeCycle.dump(out, indent, knownComets);
            }
        };
        Dumpable channels = new Dumpable()
        {
            public String dump()
            {
                return null;
            }

            public void dump(Appendable out, String indent) throws IOException
            {
                Set<String> observedChannels = getObservedChannels();
                out.append("Observed channels: ").append(String.valueOf(observedChannels.size())).append(eol);
                ContainerLifeCycle.dump(out, indent, observedChannels);
            }
        };
        ContainerLifeCycle.dump(out, indent, Arrays.asList(comets, channels));
    }

    public String toString()
    {
        return String.format("%s[%s]", getClass().getSimpleName(), getURL());
    }

    /**
     * <p>Extension that detects incoming handshakes from other Oort servers.</p>
     *
     * @see Oort#incomingCometHandshake(Map, ServerSession)
     */
    protected class OortExtension extends Extension.Adapter
    {
        @Override
        public boolean sendMeta(ServerSession to, Mutable message)
        {
            // Skip local sessions
            if (to == null)
                return true;

            if (!Channel.META_HANDSHAKE.equals(message.getChannel()))
                return true;

            // Process only successful responses
            if (!message.isSuccessful())
                return true;

            Map<String, Object> associatedExt = message.getAssociated().getExt();
            if (associatedExt == null)
                return true;

            Object associatedOortExtObject = associatedExt.get(EXT_OORT_FIELD);
            if (associatedOortExtObject instanceof Map)
            {
                @SuppressWarnings("unchecked")
                Map<String, Object> associatedOortExt = (Map<String, Object>)associatedOortExtObject;
                String remoteOortURL = (String)associatedOortExt.get(EXT_OORT_URL_FIELD);
                String cometURL = (String)associatedOortExt.get(EXT_COMET_URL_FIELD);
                String remoteOortId = (String)associatedOortExt.get(EXT_OORT_ID_FIELD);
                if (_id.equals(remoteOortId))
                {
                    // Connecting to myself: disconnect
                    _logger.warn("Detected self connect from {} to {}, disconnecting", remoteOortURL, cometURL);
                    disconnect(message);
                }
                else
                {
                    // Add the extension information even in case we're then disconnecting.
                    // The presence of the extension information will inform the client
                    // that the connection "succeeded" from the Oort point of view, but
                    // we add the advice information to drop it because if it already exists.
                    Map<String, Object> ext = message.getExt(true);
                    Map<String, Object> oortExt = new HashMap<>(2);
                    ext.put(EXT_OORT_FIELD, oortExt);
                    oortExt.put(EXT_OORT_URL_FIELD, getURL());
                    oortExt.put(EXT_OORT_ID_FIELD, getId());

                    boolean connectBack = incomingCometHandshake(Collections.unmodifiableMap(associatedOortExt), to);
                    if (connectBack)
                    {
                        String cometAliasURL = (String)associatedOortExt.get(EXT_OORT_ALIAS_URL_FIELD);
                        if (cometAliasURL != null && _pendingComets.containsKey(cometAliasURL))
                        {
                            // We are connecting to a comet that it is connecting back to us
                            // so there is not need to connect back again (just to be disconnected)
                            if (_logger.isDebugEnabled())
                                _logger.debug("Comet {} is pending with alias {}, avoiding to establish connection", remoteOortURL, cometAliasURL);
                        }
                        else
                        {
                            if (_logger.isDebugEnabled())
                                _logger.debug("Comet {} is unknown, establishing connection", remoteOortURL);
                            observeComet(remoteOortURL, cometURL);
                        }
                    }
                    else
                    {
                        disconnect(message);
                    }
                }
            }

            return true;
        }

        private void disconnect(Mutable message)
        {
            message.setSuccessful(false);
            Map<String, Object> advice = message.getAdvice(true);
            advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);
        }
    }

    /**
     * <p>This listener handles messages sent to <code>/oort/cloud</code> that contains the list of comets
     * connected to the Oort that just joined the cloud.</p>
     * <p>For example, if comets A and B are connected, and if comets C and D are connected, when connecting
     * A and C, a message is sent from A to C on <code>/oort/cloud</code> containing the comets connected
     * to A (in this case B). When C receives this message, it knows it has to connect to B also.</p>
     */
    protected class CloudListener implements ServerChannel.MessageListener
    {
        public boolean onMessage(ServerSession from, ServerChannel channel, Mutable message)
        {
            if (!from.isLocalSession())
                joinComets(message);
            return true;
        }
    }

    protected class JoinListener implements ServerChannel.MessageListener
    {
        public boolean onMessage(ServerSession from, ServerChannel channel, Mutable message)
        {
            Map<String, Object> data = message.getDataAsMap();
            String remoteOortURL = (String)data.get(EXT_OORT_URL_FIELD);
            if (remoteOortURL != null)
            {
                if (_logger.isDebugEnabled())
                    _logger.debug("Comet {} joined", remoteOortURL);
                notifyCometJoined(remoteOortURL);
            }
            return true;
        }
    }

    /**
     * <p>Listener interface that gets notified of comet events, that is when a new
     * comet joins the cloud or when a comet leaves the cloud.</p>
     * <p>If oortA and oortB form a cloud, when oortC joins to the cloud, both
     * oortA and oortB receive one event of type "comet joined", signaling that oortC
     * joined the cloud.</p>
     * <p>If, later, oortB leaves the cloud, then both oortA and oortC receive one
     * event of type "comet left" signaling that oortB left the cloud.</p>
     */
    public interface CometListener extends EventListener
    {
        /**
         * Callback method invoked when a new comet joins the cloud
         * @param event the comet event
         */
        public void cometJoined(Event event);

        /**
         * Callback method invoked when a comet leaves the cloud
         * @param event the comet event
         */
        public void cometLeft(Event event);

        /**
         * Empty implementation of {@link CometListener}
         */
        public static class Adapter implements CometListener
        {
            public void cometJoined(Event event)
            {
            }

            public void cometLeft(Event event)
            {
            }
        }

        /**
         * Comet event object delivered to {@link CometListener} methods.
         */
        public static class Event extends EventObject
        {
            private final String cometURL;

            public Event(Oort source, String cometURL)
            {
                super(source);
                this.cometURL = cometURL;
            }

            /**
             * @return the local Oort object
             */
            public Oort getOort()
            {
                return (Oort)getSource();
            }

            /**
             * @return the URL of the comet that generated the event
             */
            public String getCometURL()
            {
                return cometURL;
            }
        }
    }

    /**
     * <p>Listener that detect when a server session is removed (means that the remote
     * comet disconnected), and disconnects the OortComet associated.</p>
     */
    private class OortCometDisconnectListener implements ServerSession.RemoveListener
    {
        private final String cometURL;
        private final String remoteOortId;

        public OortCometDisconnectListener(String cometURL, String remoteOortId)
        {
            this.cometURL = cometURL;
            this.remoteOortId = remoteOortId;
        }

        @Override
        public void removed(ServerSession session, boolean timeout)
        {
            Iterator<ServerCometInfo> cometInfos = _serverComets.values().iterator();
            while (cometInfos.hasNext())
            {
                ServerCometInfo serverCometInfo = cometInfos.next();
                if (serverCometInfo.getServerSession().getId().equals(session.getId()))
                {
                    if (_logger.isDebugEnabled())
                        _logger.debug("Disconnected from comet {} with session {}", cometURL, session);
                    assert remoteOortId.equals(serverCometInfo.getOortId());
                    cometInfos.remove();

                    ClientCometInfo clientCometInfo = _clientComets.remove(remoteOortId);
                    if (clientCometInfo != null)
                        clientCometInfo.getOortComet().disconnect();

                    // Do not notify if we are stopping
                    if (isRunning())
                    {
                        String remoteOortURL = serverCometInfo.getOortURL();
                        if (_logger.isDebugEnabled())
                            _logger.debug("Comet {} left", remoteOortURL);
                        notifyCometLeft(remoteOortURL);
                    }

                    break;
                }
            }
        }
    }

    private class OortCometLoopListener implements ServerSession.MessageListener
    {
        public boolean onMessage(ServerSession session, ServerSession sender, ServerMessage message)
        {
            // Prevent loops by not delivering a message from self or Oort session to remote Oort comets
            if (session.getId().equals(sender.getId()) || isOort(sender))
            {
                if (_logger.isDebugEnabled())
                    _logger.debug("{} --| {} {}", sender, session, message);
                return false;
            }
            if (_logger.isDebugEnabled())
                _logger.debug("{} --> {} {}", sender, session, message);
            return true;
        }
    }

    private class HandshakeListener implements ClientSessionChannel.MessageListener
    {
        private final String cometURL;
        private final OortComet oortComet;

        private HandshakeListener(String cometURL, OortComet oortComet)
        {
            this.cometURL = cometURL;
            this.oortComet = oortComet;
        }

        public void onMessage(ClientSessionChannel channel, Message message)
        {
            OortComet comet = _pendingComets.get(cometURL);
            if (comet != null)
            {
                if (!message.isSuccessful())
                {
                    _logger.warn("Failed to connect to comet {}, message {}", cometURL, message);
                    Map<String, Object> advice = message.getAdvice();
                    if (advice != null && Message.RECONNECT_NONE_VALUE.equals(advice.get(Message.RECONNECT_FIELD)))
                    {
                        if (_logger.isDebugEnabled())
                            _logger.debug("Disconnecting pending comet {}", cometURL);
                        comet.disconnect();
                        // Fall through to process an eventual extension:
                        // if it was an alias URL the message will have
                        // the extension and we can map it, otherwise
                        // there will be no extension and we return
                    }
                }
            }

            Map<String,Object> ext = message.getExt();
            if (ext != null)
            {
                Object oortExtObject = ext.get(Oort.EXT_OORT_FIELD);
                if (oortExtObject instanceof Map)
                {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> oortExt = (Map<String, Object>)oortExtObject;
                    String url = (String)oortExt.get(Oort.EXT_OORT_URL_FIELD);
                    String id = (String)oortExt.get(Oort.EXT_OORT_ID_FIELD);

                    ClientCometInfo cometInfo = new ClientCometInfo(id, url, oortComet);
                    ClientCometInfo existing = _clientComets.putIfAbsent(id, cometInfo);
                    if (existing != null)
                        cometInfo = existing;

                    if (!cometURL.equals(url))
                    {
                        cometInfo.addAliasURL(cometURL);
                        if (_logger.isDebugEnabled())
                            _logger.debug("Adding alias to {}: {}", url, cometURL);
                    }

                    if (message.isSuccessful())
                        if (_logger.isDebugEnabled())
                            _logger.debug("Connected to comet {} as {} with {}/{}", url, cometURL, message.getClientId(), oortComet.getTransport());
                }
            }

            // Remove the pending comet as last step, so that if there is a concurrent
            // call to observeComet() we are sure that we always return either the
            // pending OortComet, or the connected one from the _clientComets field
            if (message.isSuccessful() || comet != null && comet.isDisconnected())
                _pendingComets.remove(cometURL);
        }
    }

    protected static abstract class CometInfo
    {
        private final String oortId;
        private final String oortURL;

        protected CometInfo(String oortId, String oortURL)
        {
            this.oortId = oortId;
            this.oortURL = oortURL;
        }

        public String getOortId()
        {
            return oortId;
        }

        public String getOortURL()
        {
            return oortURL;
        }
    }

    protected static class ServerCometInfo extends CometInfo
    {
        private final ServerSession session;

        protected ServerCometInfo(String oortId, String oortURL, ServerSession session)
        {
            super(oortId, oortURL);
            this.session = session;
        }

        public ServerSession getServerSession()
        {
            return session;
        }
    }

    protected static class ClientCometInfo extends CometInfo
    {
        private final OortComet comet;
        private final Map<String, Boolean> urls = new ConcurrentHashMap<>();

        protected ClientCometInfo(String oortId, String oortURL, OortComet comet)
        {
            super(oortId, oortURL);
            this.comet = comet;
        }

        public OortComet getOortComet()
        {
            return comet;
        }

        public void addAliasURL(String url)
        {
            urls.put(url, Boolean.TRUE);
        }

        public boolean matchesURL(String url)
        {
            return getOortURL().equals(url) || urls.containsKey(url);
        }
    }
}
TOP

Related Classes of org.cometd.oort.Oort

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.