Package org.cometd.oort

Source Code of org.cometd.oort.Seti$PresenceListener$Adapter

/*
* 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.util.ArrayList;
import java.util.Collection;
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.concurrent.CopyOnWriteArrayList;

import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.SecurityPolicy;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.AbstractService;
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.AbstractLifeCycle;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* <p>The component that Searches for Extra Terrestrial Intelligence or,
* in this case, just searches for a user logged onto a comet in an Oort cloud.</p>
* <p>Seti allows an application to maintain a mapping from userId (any application
* identifier such as user names or database IDs that represent users) to
* server sessions using the {@link #associate(String, ServerSession)} and
* {@link #disassociate(String, ServerSession)} methods.</p>
* <p>A typical example of usage of {@link Seti#associate(String, ServerSession)} is
* in a {@link SecurityPolicy} after a successful handshake (where authentication
* information can be linked with the server session), or in {@link AbstractService CometD services}
* where the association is established upon receiving a message on a particular channel
* processed by the service itself.</p>
* <p>Each comet in the cluster keeps its own mapping for clients connected to it.</p>
* <p>The {@link #sendMessage(Collection, String, Object)} and
* {@link #sendMessage(String, String, Object)} methods may be
* used to send messages to user(s) anywhere in the Oort cluster
* and Seti organizes the search in order to locate the user(s).</p>
*
* @see SetiServlet
*/
@ManagedObject("CometD cloud peer discovery component")
public class Seti extends AbstractLifeCycle implements Dumpable
{
    public static final String SETI_ATTRIBUTE = Seti.class.getName();
    private static final String SETI_ALL_CHANNEL = "/seti/all";

    private final Map<String, Set<Location>> _uid2Location = new HashMap<>();
    private final List<PresenceListener> _presenceListeners = new CopyOnWriteArrayList<>();
    private final Oort.CometListener _cometListener = new CometListener();
    private final Oort _oort;
    private final String _setiId;
    private final Logger _logger;
    private final LocalSession _session;

    public Seti(Oort oort)
    {
        _oort = oort;
        _setiId = generateSetiId(oort.getURL());
        _logger = LoggerFactory.getLogger(getClass().getName() + "." + _setiId);
        _session = oort.getBayeuxServer().newLocalSession(_setiId);
    }

    @ManagedAttribute(value = "The Oort of this Seti", readonly = true)
    public Oort getOort()
    {
        return _oort;
    }

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

    @Override
    protected void doStart() throws Exception
    {
        BayeuxServer bayeux = _oort.getBayeuxServer();

        _session.handshake();

        bayeux.createChannelIfAbsent(SETI_ALL_CHANNEL).getReference().setPersistent(true);
        _oort.observeChannel(SETI_ALL_CHANNEL);
        _session.getChannel(SETI_ALL_CHANNEL).subscribe(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                receiveBroadcast(message);
            }
        });

        String setiChannel = generateSetiChannel(_setiId);
        bayeux.createChannelIfAbsent(setiChannel).getReference().setPersistent(true);
        _session.getChannel(setiChannel).subscribe(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                receiveDirect(message);
            }
        });
        _oort.observeChannel(setiChannel);

        _oort.addCometListener(_cometListener);

        Set<String> associatedUserIds = getAssociatedUserIds();
        if (_logger.isDebugEnabled())
            _logger.debug("Broadcasting associated users {}", associatedUserIds);
        SetiPresence presence = new SetiPresence(true, associatedUserIds);
        presence.put(SetiPresence.ALIVE_FIELD, true);
        _session.getChannel(SETI_ALL_CHANNEL).publish(presence);
    }

    @Override
    protected void doStop() throws Exception
    {
        removeAssociationsAndPresences();
        _presenceListeners.clear();

        _session.disconnect();

        _oort.removeCometListener(_cometListener);

        String setiChannel = generateSetiChannel(_setiId);
        _oort.deobserveChannel(setiChannel);

        BayeuxServer bayeux = _oort.getBayeuxServer();
        ServerChannel channel = bayeux.getChannel(setiChannel);
        if (channel != null)
            channel.setPersistent(false);

        _oort.deobserveChannel(SETI_ALL_CHANNEL);
        channel = bayeux.getChannel(SETI_ALL_CHANNEL);
        if (channel != null)
            channel.setPersistent(false);
    }

    protected String generateSetiId(String oortURL)
    {
        return Oort.replacePunctuation(oortURL, '_');
    }

    protected String generateSetiChannel(String setiId)
    {
        return "/seti/" + setiId;
    }

    /**
     * <p>Associates the given userId to the given session.</p>
     * <p>If it is the first association for this userId, broadcasts this information
     * on the Oort cloud, so that other comets will know that the given userId is on this comet.</p>
     *
     * @param userId  the user identifier to associate
     * @param session the session to map the userId to
     * @return true if the session has been associated, false if it was already associated
     * @see #isAssociated(String)
     * @see #disassociate(String, ServerSession)
     */
    public boolean associate(final String userId, final ServerSession session)
    {
        if (session == null)
            throw new NullPointerException();

        LocalLocation location = new LocalLocation(userId, session);
        boolean wasAssociated = isAssociated(userId);
        boolean added = associate(userId, location);

        if (added)
        {
            session.addListener(location);
            if (_logger.isDebugEnabled())
                _logger.debug("Associated session {} to user {}", session, userId);
            if (!wasAssociated)
            {
                if (_logger.isDebugEnabled())
                    _logger.debug("Broadcasting association addition for user {}", userId);
                // Let everyone in the cluster know that this session is here
                _session.getChannel(SETI_ALL_CHANNEL).publish(new SetiPresence(true, userId));
            }
        }

        return added;
    }

    protected boolean associate(String userId, Location location)
    {
        if (!isRunning())
            return false;

        synchronized (_uid2Location)
        {
            Set<Location> locations = _uid2Location.get(userId);
            if (locations == null)
            {
                locations = new HashSet<>();
                _uid2Location.put(userId, locations);
            }
            boolean result = locations.add(location);
            if (_logger.isDebugEnabled())
                _logger.debug("Associations {}", _uid2Location);
            return result;
        }
    }

    private boolean associateRemote(String userId, SetiLocation location)
    {
        // There is a possibility that a remote Seti sends an association,
        // but immediately afterwards crashes.
        // This Seti may process the crash _before_ the association, leaving
        // this Seti with an association to a crashed Seti.
        // Here we check if, after the association, the other node is
        // still connected. If not, we disassociate. If so, and the other
        // node crashes just afterwards, the "comet left" event will take
        // care of disassociating the remote users.
        boolean associated = associate(userId, location);
        String oortURL = location._oortURL;
        if (associated && !_oort.isCometConnected(oortURL))
        {
            if (_logger.isDebugEnabled())
                _logger.debug("Disassociating {} since comet {} just disconnected", userId, oortURL);
            disassociate(userId, location);
            return false;
        }
        return associated;
    }

    /**
     * @param userId the user identifier to test for association
     * @return whether the given userId has been associated via {@link #associate(String, ServerSession)}
     * @see #associate(String, ServerSession)
     * @see #isPresent(String)
     * @see #getAssociationCount(String)
     */
    @ManagedOperation(value = "Whether the given userId is associated locally", impact = "INFO")
    public boolean isAssociated(@Name(value = "userId", description = "The userId to test for local association") String userId)
    {
        synchronized (_uid2Location)
        {
            Set<Location> locations = _uid2Location.get(userId);
            if (locations == null)
                return false;
            for (Location location : locations)
            {
                if (location instanceof LocalLocation)
                    return true;
            }
            return false;
        }
    }

    /**
     * @param userId the user identifier to test for association count
     * @return the number of local associations
     * @see #isAssociated(String)
     * @see #getPresenceCount(String)
     */
    @ManagedOperation(value = "The number of local associations for the given userId", impact = "INFO")
    public int getAssociationCount(@Name(value = "userId", description = "The userId to test for local association count") String userId)
    {
        synchronized (_uid2Location)
        {
            Set<Location> locations = _uid2Location.get(userId);
            if (locations == null)
                return 0;
            int result = 0;
            for (Location location : locations)
            {
                if (location instanceof LocalLocation)
                    ++result;
            }
            return result;
        }
    }

    /**
     * @param userId the user identifier to test for presence
     * @return whether the given userId is present on the cloud (and therefore has been associated
     * either locally or remotely)
     * @see #isAssociated(String)
     * @see #getPresenceCount(String)
     */
    @ManagedOperation(value = "Whether the given userId is present in the cloud", impact = "INFO")
    public boolean isPresent(@Name(value = "userId", description = "The userId to test for presence in the cloud") String userId)
    {
        synchronized (_uid2Location)
        {
            Set<Location> locations = _uid2Location.get(userId);
            return locations != null;
        }
    }

    /**
     * @param userId the user identifier to test for presence count
     * @return the number of associations (local or remote) on the cloud
     * @see #isPresent(String)
     * @see #getAssociationCount(String)
     */
    @ManagedOperation(value = "The number of local and remote associations for the given userId", impact = "INFO")
    public int getPresenceCount(@Name(value = "userId", description = "The userId to test for presence count") String userId)
    {
        synchronized (_uid2Location)
        {
            Set<Location> locations = _uid2Location.get(userId);
            return locations == null ? 0 : locations.size();
        }
    }

    /**
     * <p>Disassociates the given userId from the given session.</p>
     * <p>If this is the last disassociation for this userId, broadcasts this information
     * on the Oort cloud, so that other comets will know that the given userId no longer is on this comet.</p>
     *
     * @param userId the user identifier to disassociate
     * @param session the session mapped to the userId
     * @return true if the session has been disassociated, false if it was not associated
     * @see #associate(String, ServerSession)
     */
    public boolean disassociate(final String userId, ServerSession session)
    {
        LocalLocation location = new LocalLocation(userId, session);
        boolean removed = disassociate(userId, location);
        if (removed)
            if (_logger.isDebugEnabled())
                _logger.debug("Disassociated session {} from user {}", session, userId);

        // Seti is stopped before BayeuxServer, but it may happen that RemoveListeners
        // call Seti when BayeuxServer is stopping, and they will find that Seti is already stopped.
        // Do not do any action in this case, because exceptions are thrown if the action is
        // attempted (as _session is already disconnected).
        // Also, we only broadcast the presence message if no associations are left for the user,
        // because remote comets are not aware of multiple associations.
        // Consider the case where the same user is associated twice to a comet, and then only
        // one association is disassociated. The other comets do not know that the comet had multiple
        // associations, and if a presence message is sent, the remote comets will wrongly think
        // that the user is gone, while in reality it is still associated with the remaining association.
        if (_session.isConnected() && !isAssociated(userId))
        {
            if (_logger.isDebugEnabled())
                _logger.debug("Broadcasting association removal for user {}", userId);
            // Let everyone in the cluster know that this session is not here anymore
            _session.getChannel(SETI_ALL_CHANNEL).publish(new SetiPresence(false, userId));
        }

        return removed;
    }

    protected boolean disassociate(String userId, Location location)
    {
        synchronized (_uid2Location)
        {
            boolean result = false;
            Set<Location> locations = _uid2Location.get(userId);
            if (locations != null)
            {
                result = locations.remove(location);
                if (locations.isEmpty())
                    _uid2Location.remove(userId);
            }
            if (_logger.isDebugEnabled())
                _logger.debug("Associations {}", _uid2Location);
            return result;
        }
    }

    protected void removeAssociationsAndPresences()
    {
        final Set<String> userIds = new HashSet<>();
        synchronized (_uid2Location)
        {
            getAssociatedUserIds(userIds);
            _uid2Location.clear();
        }
        if (_logger.isDebugEnabled())
            _logger.debug("Broadcasting association removal for users {}", userIds);
        SetiPresence presence = new SetiPresence(false, userIds);
        presence.put(SetiPresence.ALIVE_FIELD, false);
        _session.getChannel(SETI_ALL_CHANNEL).publish(presence);
    }

    protected void removePresences(String oortURL)
    {
        List<String> userIds = new ArrayList<>();
        synchronized (_uid2Location)
        {
            for (Iterator<Map.Entry<String, Set<Location>>> entries = _uid2Location.entrySet().iterator(); entries.hasNext();)
            {
                Map.Entry<String, Set<Location>> entry = entries.next();
                Set<Location> userLocations = entry.getValue();
                for (Iterator<Location> iterator = userLocations.iterator(); iterator.hasNext(); )
                {
                    Location location = iterator.next();
                    if (location instanceof SetiLocation)
                    {
                        if (oortURL.equals(((SetiLocation)location)._oortURL))
                        {
                            iterator.remove();
                            userIds.add(entry.getKey());
                            break;
                        }
                    }
                }
                if (userLocations.isEmpty())
                    entries.remove();
            }
        }
        if (_logger.isDebugEnabled())
            _logger.debug("Removing presences of comet {} for users {}", oortURL, userIds);
        for (String userId : userIds)
            notifyPresenceRemoved(oortURL, userId);
    }

    /**
     * @return the set of {@code userId}s known to this Seti, both local and remote
     */
    @ManagedAttribute(value = "The set of userIds known to this Seti", readonly = true)
    public Set<String> getUserIds()
    {
        synchronized (_uid2Location)
        {
            return new HashSet<>(_uid2Location.keySet());
        }
    }

    /**
     * @return the set of {@code userId}s associated via {@link #associate(String, ServerSession)}
     */
    public Set<String> getAssociatedUserIds()
    {
        Set<String> result = new HashSet<>();
        getAssociatedUserIds(result);
        return result;
    }

    private void getAssociatedUserIds(Set<String> result)
    {
        synchronized (_uid2Location)
        {
            for (Map.Entry<String, Set<Location>> entry : _uid2Location.entrySet())
            {
                for (Location location : entry.getValue())
                {
                    if (location instanceof LocalLocation)
                    {
                        result.add(entry.getKey());
                        break;
                    }
                }
            }
        }
    }

    /**
     * <p>Sends a message to the given userId in the Oort cloud.</p>
     *
     * @param toUserId  the userId to send the message to
     * @param toChannel the channel to send the message to
     * @param data      the content of the message
     * @see #sendMessage(Collection, String, Object)
     */
    public void sendMessage(final String toUserId, final String toChannel, final Object data)
    {
        sendMessage(Collections.singleton(toUserId), toChannel, data);
    }

    /**
     * <p>Sends a message to multiple userIds in the Oort cloud.</p>
     *
     * @param toUserIds the userIds to send the message to
     * @param toChannel the channel to send the message to
     * @param data      the content of the message
     */
    public void sendMessage(final Collection<String> toUserIds, final String toChannel, final Object data)
    {
        for (String toUserId : toUserIds)
        {
            Set<Location> copy = new HashSet<>();
            synchronized (_uid2Location)
            {
                Set<Location> locations = _uid2Location.get(toUserId);
                if (locations == null)
                    copy.add(new SetiLocation(toUserId, null));
                else
                    copy.addAll(locations);
            }

            if (_logger.isDebugEnabled())
                _logger.debug("Sending message to locations {}", copy);
            for (Location location : copy)
                location.send(toUserId, toChannel, data);
        }
    }

    /**
     * <p>Receives messages directly from other Setis in the cloud, containing
     * messages to be delivered to sessions connected to this comet.</p>
     *
     * @param message the message to deliver to a session connected to this comet
     */
    protected void receiveDirect(Message message)
    {
        if (_logger.isDebugEnabled())
            _logger.debug("Received direct message {}", message);
        Map<String, Object> data = message.getDataAsMap();
        Boolean presence = (Boolean)data.get(SetiPresence.PRESENCE_FIELD);
        if (presence != null)
            receivePresence(data);
        else
            receiveMessage(data);
    }

    /**
     * <p>Receives messages broadcast by other Setis in the cloud.</p>
     * <p>Broadcast messages may be presence messages, where another Seti advertises
     * an association, or fallback messages.
     * Fallback messages are messages that were sent to a particular Seti because the
     * sender thought the target userId was there, but the receiving Seti does not know
     * that userId anymore (for example, it just disconnected); in this case, the receiving
     * Seti broadcasts the message to the whole cloud, in the hope that the user can be
     * found in some other comet of the cloud.</p>
     *
     * @param message the message to possibly deliver to a session connected to this comet
     */
    protected void receiveBroadcast(Message message)
    {
        if (_logger.isDebugEnabled())
            _logger.debug("Received broadcast message {}", message);
        Map<String, Object> data = message.getDataAsMap();
        Boolean presence = (Boolean)data.get(SetiPresence.PRESENCE_FIELD);
        if (presence != null)
            receivePresence(data);
        else
            receiveMessage(data);
    }

    /**
     * <p>Receives a presence message.</p>
     *
     * @param presence the presence message received
     */
    protected void receivePresence(Map<String, Object> presence)
    {
        if (_logger.isDebugEnabled())
            _logger.debug("Received presence message {}", presence);
        String oortURL = (String)presence.get(SetiPresence.OORT_URL_FIELD);
        if (_setiId.equals(generateSetiId(oortURL)))
            receiveLocalPresence(presence);
        else
            receiveRemotePresence(presence);
    }

    private void receiveLocalPresence(Map<String, Object> presence)
    {
        String oortURL = (String)presence.get(SetiPresence.OORT_URL_FIELD);
        boolean present = (Boolean)presence.get(SetiPresence.PRESENCE_FIELD);
        Set<String> userIds = convertPresenceUsers(presence);
        if (_logger.isDebugEnabled())
            _logger.debug("Notifying presence listeners {}", presence);
        for (String userId : userIds)
        {
            if (present)
                notifyPresenceAdded(oortURL, userId);
            else
                notifyPresenceRemoved(oortURL, userId);
        }
    }

    private void receiveRemotePresence(Map<String, Object> presence)
    {
        String oortURL = (String)presence.get(SetiPresence.OORT_URL_FIELD);
        boolean present = (Boolean)presence.get(SetiPresence.PRESENCE_FIELD);
        Set<String> userIds = convertPresenceUsers(presence);

        if (_logger.isDebugEnabled())
            _logger.debug("Received remote presence message from comet {} for {}", oortURL, userIds);

        for (String userId : userIds)
        {
            SetiLocation location = new SetiLocation(userId, oortURL);
            if (present)
            {
                if (associateRemote(userId, location))
                    notifyPresenceAdded(oortURL, userId);
            }
            else
            {
                if (disassociate(userId, location))
                    notifyPresenceRemoved(oortURL, userId);
            }
        }

        if (presence.get(SetiPresence.ALIVE_FIELD) == Boolean.TRUE)
        {
            // Message sent on startup by the remote Seti, push our associations
            OortComet oortComet = _oort.findComet(oortURL);
            if (oortComet != null)
            {
                Set<String> associatedUserIds = getAssociatedUserIds();
                if (_logger.isDebugEnabled())
                    _logger.debug("Pushing associated users {} to comet {}", associatedUserIds, oortURL);
                ClientSessionChannel channel = oortComet.getChannel(generateSetiChannel(generateSetiId(oortURL)));
                channel.publish(new SetiPresence(true, associatedUserIds));
            }
        }
    }

    public void addPresenceListener(PresenceListener listener)
    {
        _presenceListeners.add(listener);
    }

    public void removePresenceListener(PresenceListener listener)
    {
        _presenceListeners.remove(listener);
    }

    private void notifyPresenceAdded(String oortURL, String userId)
    {
        PresenceListener.Event event = new PresenceListener.Event(this, userId, oortURL);
        for (PresenceListener listener : _presenceListeners)
        {
            try
            {
                listener.presenceAdded(event);
            }
            catch (Throwable x)
            {
                _logger.info("Exception while invoking listener " + listener, x);
            }
        }
    }

    private void notifyPresenceRemoved(String oortURL, String userId)
    {
        PresenceListener.Event event = new PresenceListener.Event(this, userId, oortURL);
        for (PresenceListener listener : _presenceListeners)
        {
            try
            {
                listener.presenceRemoved(event);
            }
            catch (Throwable x)
            {
                _logger.info("Exception while invoking listener " + listener, x);
            }
        }
    }

    /**
     * <p>Receives a seti message.</p>
     *
     * @param message the seti message received
     */
    protected void receiveMessage(Map<String, Object> message)
    {
        String userId = (String)message.get(SetiMessage.USER_ID_FIELD);
        String channel = (String)message.get(SetiMessage.CHANNEL_FIELD);
        Object data = message.get(SetiMessage.DATA_FIELD);

        Set<Location> copy = new HashSet<>();
        synchronized (_uid2Location)
        {
            Set<Location> locations = _uid2Location.get(userId);
            if (locations != null)
            {
                // Consider cometA, cometB and cometC and a user that is associated
                // in both cometA and cometB. When cometC sends a message to the user,
                // it knows that the user is in both cometA and cometB (thanks to presence
                // messages) and will send a message to both cometA and cometB.
                // But cometA also knows from presence messages that the user is also in
                // cometB and should not forward the message arriving from cometC to cometB
                // since cometC will take care of sending to cometB.
                // Hence, we forward the message only locally
                for (Location location : locations)
                {
                    if (location instanceof LocalLocation)
                        copy.add(location);
                }
            }
        }

        if (_logger.isDebugEnabled())
            _logger.debug("Received message {} for locations {}", message, copy);
        for (Location location : copy)
            location.receive(userId, channel, data);
    }

    @SuppressWarnings("unchecked")
    private Set<String> convertPresenceUsers(Map<String, Object> presence)
    {
        Object value = presence.get(SetiPresence.USER_IDS_FIELD);
        if (value instanceof Set)
            return (Set<String>)value;
        if (value instanceof Collection)
            return new HashSet<>((Collection<? extends String>)value);
        if (value.getClass().isArray())
        {
            Set<String> result = new HashSet<>();
            for (Object item : (Object[])value)
                result.add(item.toString());
            return result;
        }
        throw new IllegalArgumentException();
    }

    public String dump()
    {
        return ContainerLifeCycle.dump(this);
    }

    public void dump(Appendable out, String indent) throws IOException
    {
        ContainerLifeCycle.dumpObject(out, this);
        List<String> state = new ArrayList<>();
        synchronized (_uid2Location)
        {
            for (Map.Entry<String, Set<Location>> entry : _uid2Location.entrySet())
                state.add(String.format("%s @ %s", entry.getKey(), entry.getValue()));
        }
        ContainerLifeCycle.dump(out, indent, state);
    }

    @Override
    public String toString()
    {
        return String.format("%s[%s]", getClass().getName(), getId());
    }

    /**
     * <p>The representation of where a user is.</p>
     */
    protected interface Location
    {
        public void send(String toUser, String toChannel, Object data);

        public void receive(String toUser, String toChannel, Object data);

        public int hashCode();

        public boolean equals(Object obj);
    }

    /**
     * <p>A Location that represent a user connected to a local comet.</p>
     */
    protected class LocalLocation implements Location, ServerSession.RemoveListener
    {
        private final String _userId;
        private final ServerSession _session;

        protected LocalLocation(String userId, ServerSession session)
        {
            _userId = userId;
            _session = session;
        }

        public void send(String toUser, String toChannel, Object data)
        {
            _session.deliver(Seti.this._session.getServerSession(), toChannel, data);
        }

        public void receive(String toUser, String toChannel, Object data)
        {
            send(toUser, toChannel, data);
        }

        @Override
        public void removed(ServerSession session, boolean timeout)
        {
            disassociate(_userId, session);
        }

        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
                return true;
            if (!(obj instanceof LocalLocation))
                return false;
            LocalLocation that = (LocalLocation)obj;
            return _userId.equals(that._userId) && _session.getId().equals(that._session.getId());
        }

        @Override
        public int hashCode()
        {
            return 31 * _userId.hashCode() + _session.getId().hashCode();
        }

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

    /**
     * <p>A Location that represent a user connected to a remote comet.</p>
     */
    protected class SetiLocation implements Location
    {
        private final String _userId;
        private final String _oortURL;
        private final String _setiChannel;

        protected SetiLocation(String userId, String oortURL)
        {
            _userId = userId;
            _oortURL = oortURL;
            _setiChannel = oortURL == null ? SETI_ALL_CHANNEL : generateSetiChannel(generateSetiId(oortURL));
        }

        public void send(String toUser, String toChannel, Object data)
        {
            _session.getChannel(_setiChannel).publish(new SetiMessage(toUser, toChannel, data));
        }

        public void receive(String toUser, String toChannel, Object data)
        {
            // A message has been sent to this comet because the sender thought
            // the user was in this comet. If it were, we would have found a
            // LocalLocation, but instead found this SetiLocation.
            // Therefore, the user must have moved to this seti location, and
            // we forward the message.
            send(toUser, toChannel, data);
        }

        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
                return true;
            if (!(obj instanceof SetiLocation))
                return false;
            SetiLocation that = (SetiLocation)obj;
            return _userId.equals(that._userId) && _setiChannel.equals(that._setiChannel);
        }

        @Override
        public int hashCode()
        {
            return 31 * _userId.hashCode() + _setiChannel.hashCode();
        }

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

    private class SetiMessage extends HashMap<String, Object>
    {
        private static final String USER_ID_FIELD = "userId";
        private static final String CHANNEL_FIELD = "channel";
        private static final String SETI_ID_FIELD = "setiId";
        private static final String DATA_FIELD = "data";

        private SetiMessage(String toUser, String toChannel, Object data)
        {
            super(4);
            put(USER_ID_FIELD, toUser);
            put(CHANNEL_FIELD, toChannel);
            put(SETI_ID_FIELD, _setiId);
            put(DATA_FIELD, data);
        }
    }

    private class SetiPresence extends HashMap<String, Object>
    {
        private static final String USER_IDS_FIELD = "userIds";
        private static final String OORT_URL_FIELD = "oortURL";
        private static final String ALIVE_FIELD = "alive";
        private static final String PRESENCE_FIELD = "presence";

        private SetiPresence(boolean present, String userId)
        {
            this(present, Collections.singleton(userId));
        }

        private SetiPresence(boolean present, Set<String> userIds)
        {
            super(4);
            put(USER_IDS_FIELD, userIds);
            put(OORT_URL_FIELD, _oort.getURL());
            put(PRESENCE_FIELD, present);
        }
    }

    /**
     * Listener interface that gets notified of remote Seti presence events.
     */
    public interface PresenceListener extends EventListener
    {
        /**
         * Callback method invoked when a presence is added to a remote Seti
         * @param event the presence event
         */
        public void presenceAdded(Event event);

        /**
         * Callback method invoked when a presence is removed from a remote Seti
         * @param event the presence event
         */
        public void presenceRemoved(Event event);

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

            public void presenceRemoved(Event event)
            {
            }
        }

        /**
         * Seti presence event object, delivered to {@link PresenceListener} methods.
         */
        public static class Event extends EventObject
        {
            private final String userId;
            private final String url;

            public Event(Seti source, String userId, String url)
            {
                super(source);
                this.userId = userId;
                this.url = url;
            }

            /**
             * @return the local Seti object
             */
            public Seti getSeti()
            {
                return (Seti)getSource();
            }

            /**
             * @return the userId associated to this presence event
             */
            public String getUserId()
            {
                return userId;
            }

            /**
             * @return the Oort URL where this presence event happened
             */
            public String getOortURL()
            {
                return url;
            }

            /**
             * @return whether this presence event happened on the local Seti or on a remote Seti
             */
            public boolean isLocal()
            {
                return getSeti().getOort().getURL().equals(getOortURL());
            }

            @Override
            public String toString()
            {
                return String.format("%s[%s %s on %s]",
                        getClass().getName(),
                        getUserId(),
                        isLocal() ? "local" : "remote",
                        getSeti());
            }
        }
    }

    private class CometListener implements Oort.CometListener
    {
        public void cometJoined(Event event)
        {
            String oortURL = event.getCometURL();
            OortComet oortComet = _oort.findComet(oortURL);
            if (_logger.isDebugEnabled())
                _logger.debug("Comet joined: {} with {}", oortURL, oortComet);
            if (oortComet != null)
            {
                ClientSessionChannel channel = oortComet.getChannel(generateSetiChannel(generateSetiId(oortURL)));
                Set<String> userIds = getAssociatedUserIds();
                if (_logger.isDebugEnabled())
                    _logger.debug("Pushing associated users {} to comet {}", userIds, oortURL);
                channel.publish(new SetiPresence(true, userIds));
            }
        }

        public void cometLeft(Event event)
        {
            String oortURL = event.getCometURL();
            if (_logger.isDebugEnabled())
                _logger.debug("Comet left: {}", oortURL);
            removePresences(oortURL);
        }
    }
}
TOP

Related Classes of org.cometd.oort.Seti$PresenceListener$Adapter

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.