Package org.jdesktop.wonderland.server.comms

Source Code of org.jdesktop.wonderland.server.comms.WonderlandSessionListener

/**
* Project Wonderland
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied
* this code.
*/
package org.jdesktop.wonderland.server.comms;

import com.sun.sgs.app.AppContext;
import com.sun.sgs.app.Channel;
import com.sun.sgs.app.ChannelManager;
import com.sun.sgs.app.ClientSession;
import com.sun.sgs.app.ClientSessionListener;
import com.sun.sgs.app.DataManager;
import com.sun.sgs.app.Delivery;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.ManagedObjectRemoval;
import com.sun.sgs.app.ManagedReference;
import com.sun.sgs.app.Task;
import com.sun.sgs.app.TaskManager;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jdesktop.wonderland.common.ExperimentalAPI;
import org.jdesktop.wonderland.common.auth.WonderlandIdentity;
import org.jdesktop.wonderland.common.comms.ConnectionType;
import org.jdesktop.wonderland.common.comms.SessionInternalConnectionType;
import org.jdesktop.wonderland.common.comms.messages.AttachClientMessage;
import org.jdesktop.wonderland.common.comms.messages.AttachedClientMessage;
import org.jdesktop.wonderland.common.comms.messages.DetachClientMessage;
import org.jdesktop.wonderland.common.comms.messages.SessionInitializationMessage;
import org.jdesktop.wonderland.common.messages.ErrorMessage;
import org.jdesktop.wonderland.common.messages.Message;
import org.jdesktop.wonderland.common.messages.MessageID;
import org.jdesktop.wonderland.common.messages.MessagePacker;
import org.jdesktop.wonderland.common.messages.MessagePacker.PackerException;
import org.jdesktop.wonderland.common.messages.MessagePacker.ReceivedMessage;
import org.jdesktop.wonderland.common.security.Action;
import org.jdesktop.wonderland.common.security.annotation.Actions;
import org.jdesktop.wonderland.server.UserMO;
import org.jdesktop.wonderland.server.UserManager;
import org.jdesktop.wonderland.server.auth.ClientIdentityManager;
import org.jdesktop.wonderland.server.security.ActionMap;
import org.jdesktop.wonderland.server.security.Resource;
import org.jdesktop.wonderland.server.security.ResourceMap;
import org.jdesktop.wonderland.server.security.SecureTask;
import org.jdesktop.wonderland.server.security.SecurityManager;

/**
* This is the default session listener is used by Wonderland clients.
* Clients can select this listener by specifiying "wonderland_client" in
* the protocol selection message.
* <p>
* The WonderlandSessionListener supports an extensible set of handlers.
* Hanlders can be registered to service for specific message types.  These
* messages will be reported for all sessions connected to the handler.
* Note that handlers are not dynamic: handlers must be registered before
* the first session is created.  Handlers added after a session is created
* will not be used in this session.
* <p>
* Other convenience methods exist to send messages to all clients.
*
* @author jkaplan
*/
@ExperimentalAPI
public class WonderlandSessionListener
        implements ClientSessionListener, ManagedObject,
                   ManagedObjectRemoval, Serializable
{   
    /** a logger */
    private static final Logger logger =
            Logger.getLogger(WonderlandSessionListener.class.getName());

    /** the name of the binding for this listener */
    private static final String BINDING_NAME =
            WonderlandSessionListener.class.getName();
   
    /** client ID of the internal session handler */
    private static final short SESSION_INTERNAL_CLIENT_ID =
            SessionInternalConnectionType.SESSION_INTERNAL_CLIENT_ID;

    /** a cache of Actions associated with message classes */
    private static final Map<Class, Set<Action>> actionCache =
            new HashMap<Class, Set<Action>>();

    /** the session associated with this listener */
    private ManagedReference<ClientSession> sessionRef;
   
    /** a map from the ID we've assigned a client to the handler for that
        client */
    private Map<Short, ClientHandlerRef> handlers;
   
    /** a map from the ID we've assigned a client to the sender for that
        client */
    private Map<Short, WonderlandClientSenderImpl> senders;

    /** the unique ID of this session listener */
    private BigInteger sessionID;

    /**
     * Create a new instance of WonderlandSessionListener for the given
     * session
     * @param session the session connected to this listener
     */
    public WonderlandSessionListener(ClientSession session) {
        DataManager dm = AppContext.getDataManager();
        sessionRef = dm.createReference(session);
       
        if (logger.isLoggable(Level.FINE)) {
            logger.finest("New session listener for " + session.getName());
        }
       
        // initialize maps
        handlers = new TreeMap<Short, ClientHandlerRef>();
        senders = new TreeMap<Short, WonderlandClientSenderImpl>();

        // create a binding for ourself in the datastore.  This binding
        // is used by inner classes to securely complete operations on this
        // listener
        dm.setBinding(getBindingName(), this);

        // add internal handler
        ClientHandlerRef internalRef = getHandlerStore().getHandlerRef(
                    SessionInternalConnectionType.SESSION_INTERNAL_CLIENT_TYPE);
        ((SessionInternalConnectionHandler) internalRef.get()).setListener(this);
        handlers.put(SESSION_INTERNAL_CLIENT_ID, internalRef);
       
        // send an initialization message to the client over the internal
        // connection.  The client's unique ID is the ID of the ClientSession
        // managed object.  This id is guaranteed by Darkstar to be unique
        // across the whole Darkstar cluster.
        sessionID = sessionRef.getId();
        WonderlandIdentity userID =
               AppContext.getManager(ClientIdentityManager.class).getClientID();
        Message sim = new SessionInitializationMessage(sessionID, userID);
        sendToSession(SESSION_INTERNAL_CLIENT_ID, sim);
    }
       
    /**
     * Initialize the session listener
     */
    public static void initialize() {
        logger.fine("Initialize WonderlandSessionListener");
       
        DataManager dm = AppContext.getDataManager();
       
        // create store for registered handlers
        dm.setBinding(HandlerStore.DS_KEY, new HandlerStore());
   
        // register the internal handler
        SessionInternalConnectionHandler internal = new SessionInternalConnectionHandler();
        registerClientHandler(internal);
    }
   
    /**
     * Clean up when a session is destroyed
     */
    public void removingObject() {
        // remove our binding
        AppContext.getDataManager().removeBinding(getBindingName());
    }

    /**
     * Get the session id of this session
     * @return this session listener's unique identifier
     */
    public BigInteger getSessionID() {
        return sessionID;
    }

    /**
     * Called when the listener receives a message.  If the wrapped session
     * has not yet been defined, look for ProtocolSelectionMessages, otherwise
     * simply forward the data to the delegate session
     * @param data the message data
     */
    public void receivedMessage(ByteBuffer data) {
        try {
            // extract the message and client id
            ReceivedMessage recv = MessagePacker.unpack(data);
            Message m = recv.getMessage();
            short clientID = recv.getClientID();
           
            // find the handler
            ClientConnectionHandler handler = getHandler(clientID);
            if (handler == null) {
                logger.fine("Session " + getSession().getName() +
                            " unknown handler ID: " + clientID);
                sendError(m.getMessageID(), clientID,
                          "Unknown handler ID: " + clientID);
                return;
            }
           
            if (logger.isLoggable(Level.FINEST)) {
                logger.finest("Session " + getSession().getName() +
                              " received message " + m +
                              " for client ID" + clientID +
                              " handled by " + handler.getConnectionType());
            }

            // determine if security is needed
            Resource resource = null;
            if (handler instanceof SecureClientConnectionHandler) {
                SecureClientConnectionHandler sec =
                        (SecureClientConnectionHandler) handler;
                resource = sec.checkMessage(getWonderlandClientID(), m);
            }

            // get the actions associated with this message
            Set<Action> actions = getActions(m.getClass());

            if (logger.isLoggable(Level.FINEST)) {
                logger.finest("Session " + getSession().getName() +
                              " security for message " + m +
                              " resource: " + resource +
                              " actions: " + actions.size());
            }

            // if the resource is not null and the message requires actions,
            // we need to query the resource for the given actions, and only
            // handle the message if the query returns the required
            // permissions
            if (resource != null && !actions.isEmpty()) {
                receiveSecure(resource, clientID, m, actions);
            } else {
                // no security, just handle the message
                WonderlandClientSender sender = senders.get(clientID);
                handler.messageReceived(sender, getWonderlandClientID(), m);
            }
        } catch (PackerException eme) {
            logger.log(Level.WARNING, "Error extracting message from client",
                       eme);
           
            // if possible, send a reply to the client
            if (eme.getMessageID() != null) {
                sendError(eme.getMessageID(), eme.getClientID(), eme);
            }
        }
    }

    /**
     * Set up security and make a secure message request.  This will spawn a new
     * task to handle the message request if the security check succeeds.
     * @param resource the resource to use in the check
     * @param clientID the clientID of the
     * @param message the message to deliver
     */
    private void receiveSecure(final Resource resource,
                               final short clientID,
                               final Message message,
                               final Set<Action> actions)
    {
        // get the security manager
        SecurityManager security = AppContext.getManager(SecurityManager.class);

        // create a request
        ActionMap am = new ActionMap(resource, actions.toArray(new Action[0]));
        ResourceMap request = new ResourceMap();
        request.put(resource.getId(), am);

        // perform the security check
        security.doSecure(request, new ReceiveSecureTask(resource.getId(),
                                                         clientID, actions,
                                                         message,
                                                         getBindingName()));
    }

    /**
     * Called when the delegate session is disconnected
     * @param forced true if the disconnect was forced
     */
    public void disconnected(boolean forced) {
        // mark ourself for update
        AppContext.getDataManager().markForUpdate(this);

        // Detach all handlers. Convert IDs to an array first because
        // the map is modified in the handleDetach method, which causes
        // a concurrent modification exception if we are iterating directly
        // over the key set
        Short[] clientIDs = handlers.keySet().toArray(new Short[0]);
        for (Short clientID : clientIDs) {
            handleDetach(clientID.shortValue(), true);
        }
       
        // clear the list
        handlers.clear();
    }

    /**
     * Register a handler that will handle connections from a particular
     * WonderlandClient type.
     * @see org.jdesktop.wonderland.server.comms.CommsManager#registerClientHandler(ClientConnectionHandler)
     *
     * @param handler the handler to register
     */
    public static void registerClientHandler(ClientConnectionHandler handler) {
        logger.fine("Register client handler for type " +
                    handler.getConnectionType());
       
        HandlerStore store = getHandlerStore();
        store.register(handler);
               
        // let Darkstar know this is an update       
        AppContext.getDataManager().markForUpdate(store);
       
        // notify the handler
        handler.registered(store.getSender(handler.getConnectionType()));
    }
   
    /**
     * Unregister a client handler that was previously registered
     * @param handler the handler to unregister
     */
    public static void unregisterClientHandler(ClientConnectionHandler handler) {
        logger.fine("Unregister client handler for type " +
                    handler.getConnectionType());
   
        getHandlerStore().unregister(handler);
    }
   
    /**
     * Get the handler for the given type
     * @param clientType the type of client to get a handler for
     * @return the handler for the given type, or null if no handler
     * is registered for the given type
     */
    public static ClientConnectionHandler getClientHandler(ConnectionType clientType) {
        ClientHandlerRef ref = getHandlerStore().getHandlerRef(clientType);
        if (ref == null) {
            return null;
        }
       
        return ref.get();
    }
   
    /**
     * Get all client handlers
     * @return the set of all client handlers
     */
    public static Set<ClientConnectionHandler> getClientHandlers() {
        return getHandlerStore().getHandlers();
    }
   
    /**
     * Get a sender that can be used to send messages to all clients
     * of a given ConnectionType
     * @see org.jdesktop.wonderland.server.comms.CommsManager#getSender(ConnectionType)
     *
     * @param type the type of client to get a channel to
     * @return a sender for sending to all clients of the given type
     * @throws IllegalStateException if no handler is registered for the given
     * type
     */
    public static WonderlandClientSender getSender(ConnectionType type) {
        WonderlandClientSender sender = getHandlerStore().getSender(type);
        if (sender == null) {
            throw new IllegalStateException("No handler registered for " + type);
        }
       
        return sender;
    }

    /**
     * Get the session this listener represents.
     * @return the session connected to this listener
     */
    protected ClientSession getSession() {
        return sessionRef.get();
    }

    /**
     * Get the WonderlandClientID for this session
     * @return the WonderlandClientID
     */
    protected WonderlandClientID getWonderlandClientID() {
        return new WonderlandClientID(sessionRef);
    }

    /**
     * Get a client handler by client ID
     * @param clientID the id of the client to get
     * @return the handler for the given ID, or null if there is no
     * handler for the given ID
     */
    protected ClientConnectionHandler getHandler(Short clientID) {
        ClientHandlerRef ref = handlers.get(clientID);
        if (ref == null) {
            return null;
        }
       
        return ref.get();
    }
   
    /**
     * Remove the client handler with the given clientID
     * @param clientID the id of the client to remove
     * @return the removed handler for the given ID, or null if there is no
     * handler for the given ID
     */
    private ClientHandlerRef removeHandler(Short clientID) {
        return handlers.remove(clientID);
    }
   
    /**
     * Handle an attach request
     * @param messageID the ID of the message to respond to
     * @param type the type of client to attach
     * @param properties the message properties
     */
    protected void handleAttach(MessageID messageID,
                                ConnectionType type,
                                Properties properties)
    {
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("Session " + getSession().getName() + " attach " +
                        "client type " + type);
        }

        // make sure properties is not null
        if (properties == null) {
            properties = new Properties();
        }

        // get the handler for this type
        final ClientHandlerRef ref = getHandlerStore().getHandlerRef(type);
        if (ref == null) {
            logger.fine("Session " + getSession().getName() + " no handler " +
                        "for client type " + type);
            sendError(messageID, SESSION_INTERNAL_CLIENT_ID,
                      "No handler for " + type);
            return;
        }

        // determine if security is needed
        Resource resource = null;
        if (ref.get() instanceof SecureClientConnectionHandler) {
            SecureClientConnectionHandler sec =
                    (SecureClientConnectionHandler) ref.get();
            resource = sec.checkConnect(getWonderlandClientID(), properties);
        }

        // if the resource is not null, we need to query the connect capability
        // and only connect if it succeeds
        if (resource != null) {
            attachSecure(resource, messageID, type, properties, ref);
        } else {
            // no security, just finish the connection
            finishAttach(messageID, type, properties, ref);
        }
    }

    /**
     * Set up security and make an attach request.  This will spawn a new task
     * to handle the attach request if the security check succeeds.
     * @param resource the resource to use in the check
     * @param messageID the ID of the attach message
     * @param type the type of connection
     * @param properties the initial connection properties
     * @param ref the handler we will use for this message
     */
    private void attachSecure(final Resource resource,
                              final MessageID messageID,
                              final ConnectionType type,
                              final Properties properties,
                              final ClientHandlerRef ref)
    {
        // get the security manager
        SecurityManager security = AppContext.getManager(SecurityManager.class);

        // create a request
        ActionMap am = new ActionMap(resource, ConnectAction.getInstance());
        ResourceMap request = new ResourceMap();
        request.put(resource.getId(), am);

        // perform the security check
        security.doSecure(request, new AttachSecureTask(resource.getId(),
                                                        messageID, type,
                                                        properties, ref,
                                                        getBindingName()));
    }

    /**
     * Complete the attach process, after the security check (if any) has been
     * performed.
     * @param messageID the id of the connect message
     * @param type the type of connection
     * @param properties the initial connection properties
     * @param ref a reference to the correct client handler
     */
    private void finishAttach(MessageID messageID, ConnectionType type,
                              Properties properties, ClientHandlerRef ref)
    {
        ClientSession session = getSession();

        // get the ID for this type
        WonderlandClientSenderImpl sender = getHandlerStore().getSender(type);
        short clientID = sender.getClientID();
       
        // make sure this isn't a duplicate join
        if (handlers.containsKey(Short.valueOf(clientID))) {
            logger.fine("Session " + session.getName() + " duplicate client " +
                        "for type " + type);
            sendError(messageID, SESSION_INTERNAL_CLIENT_ID,
                          "Duplicate client for " + type);
            return;
        }

        // mark ourself for update
        AppContext.getDataManager().markForUpdate(this);

        // add handler to our list
        handlers.put(Short.valueOf(clientID), ref);
       
        // send response message
        Message resp = new AttachedClientMessage(messageID, clientID);
        sendToSession(SESSION_INTERNAL_CLIENT_ID, resp);
       
        // add this session to the sender
        sender.addSession(session);
       
        // Save the WonderlandChannel that we can use to communicate
        // with this cell.  Store the channel locally since it is used in
        // every call to messageReceived()
        senders.put(clientID, sender);
       
        // notify the handler
        ref.get().clientConnected(sender, getWonderlandClientID(), properties);
    }
   
    /**
     * Handle a detach request
     * @param clientID the id of the client to detach
     * @param disconnect if true, this is a disconnect.  In that case, the
     * ClientSession will be removed from the channel automatically, so
     * there is no need for us to do it explicitly.
     */
    protected void handleDetach(short clientID, boolean disconnect) {
        ClientConnectionHandler handler = getHandler(Short.valueOf(clientID));
        if (handler == null) {
            logger.fine("Detach unknown client ID " + clientID);
            return;
        }
       
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("Session " + getSession().getName() + " detach " +
                        "client type " + handler.getConnectionType());
        }

        // remove this client from the sender
        WonderlandClientSenderImpl sender = senders.remove(clientID);
        if (sender == null && clientID != SESSION_INTERNAL_CLIENT_ID) {
            logger.warning("Null sender for " + handler.getConnectionType());
        }

        // skip removing the session if this is a disconnect. In the disconnect
        // case, Darkstar automatically removes the ClientSession from any
        // channels, so we don't need to do it ourselves (doing it ourselves
        // causes extra conflict in Darkstar).
        if (!disconnect && sender != null) {
            sender.removeSession(getSession());
        }

        // remove the handler from the map
        ClientHandlerRef handlerRef =
                removeHandler(Short.valueOf(clientID));

        // notify the handler
        if (disconnect) {
            // in the case of a disconnect, we don't want to do all the
            // notifications in a single task.  Instead, we add each
            // notification to the user's logout task list.
            scheduleClientDisconnect(handlerRef, sender);
        } else {
            // a single client disconnect.  Just notify the handler directly
            handler.clientDisconnected(sender, getWonderlandClientID());
        }
    }

    /**
     * Schedule a disconnect task with the user's logout task list
     * @param handlerRef a reference to the handler to remove
     * @param sender the sender to send responses to
     */
    private void scheduleClientDisconnect(ClientHandlerRef handlerRef,
                                          WonderlandClientSender sender)
    {
        // get the list of logout tasks for this user
        UserMO user = UserManager.getUserManager().getUser(getWonderlandClientID());
        Queue<Task> tasks = user.getLogoutTasks(getWonderlandClientID());

        // add a new task to notify the handler of the disconnect
        tasks.add(new NotifyDisconnectTask(getWonderlandClientID(),
                                           handlerRef, sender));
    }

    /**
     * Get the binding name this manager is bound to
     * @return the binding name
     */
    protected String getBindingName() {
        return BINDING_NAME + "." + sessionRef.getId();
    }

    /**
     * Send an error to the session
     * @param messageID the source message's ID
     * @param clientID the client ID to send to
     * @param error the error to send
     */
    protected void sendError(MessageID messageID, short clientID, String error)
    {
        sendError(messageID, clientID, error, null);
    }
   
    /**
     * Send an error to the session
     * @param messageID the source message's ID
     * @param clientID the client ID to send to
     * @param cause the cause of the error
     */
    protected void sendError(MessageID messageID, short clientID,
                             Throwable cause)
    {
        sendError(messageID, clientID, null, cause);
    }
   
    /**
     * Send an error to the session
     * @param messageID the messageID of the original error
     * @param clientID the client ID to send to
     * @param error the error message
     * @param cause the underlying exception
     */
    protected void sendError(MessageID messageID, short clientID,
                             String error, Throwable cause)
    {
        ErrorMessage msg = new ErrorMessage(messageID, error, cause);
        sendToSession(clientID, msg);
    }
  
    /**
     * Send a message to the session channel using the given client ID.
     * @param clientID the client ID to use when sending
     * @param message the message to send
     * @throws IllegalArgumentException if there is an error serializing
     * the given message
     */
    protected void sendToSession(short clientID, Message message) {
        if (logger.isLoggable(Level.FINEST)) {
            logger.finest("Session " + getSession().getName() + " send " +
                          "message " + message + " to client " + clientID);
        }
       
        getSession().send(serializeMessage(message, clientID));
    }
       
    /**
     * Serialize the given message into a ByteBuffer to send to a client of the
     * given type.
     * @param message the message to serialize
     * @param clientID the clientID of the handler to send it to
     * @return a ByteBuffer containing the serialized message
     * @throws IllegalArgumentException if there is an error serializing the
     * given message
     */
    private static ByteBuffer serializeMessage(Message message, short clientID)
    {
        try {
            return MessagePacker.pack(message, clientID);
        } catch (PackerException ioe) {
            throw new IllegalArgumentException("Error serializing " + message,
                                               ioe);
        }   
    }
   
    /**
     * Get the store of registered handlers
     * @return the store of registered handlers
     */
    private static HandlerStore getHandlerStore() {
        return (HandlerStore) AppContext.getDataManager().getBinding(HandlerStore.DS_KEY);
    }

    /**
     * Get the actions associated with a message class.
     * <p>
     * Note this method is static, and affects static data, not the data
     * associated with any particular Darkstar managed object.
     * @param clazz the message class
     * @return the set of actions associated with the given message type, or
     * an empty set if no actions are associated with the given type
     */
    private static Set<Action> getActions(Class clazz) {
        Set<Action> out;

        synchronized (actionCache) {
            out = actionCache.get(clazz);
        }

        if (out == null) {
            out = new HashSet<Action>();
           
            // get the actions from the actions annotation
            Actions actions = (Actions) clazz.getAnnotation(Actions.class);
            if (actions != null) {
                for (Class<Action> c : actions.value()) {
                    try {
                        out.add(c.newInstance());
                    } catch (InstantiationException ie) {
                        throw new IllegalStateException("Error getting " +
                                " actions for " + clazz, ie);
                    } catch (IllegalAccessException iae) {
                        throw new IllegalStateException("Error getting " +
                                " actions for " + clazz, iae);
                    }
                }
            }

            // walk up the object tree searching for actions
            if (clazz.getSuperclass() != null) {
                out.addAll(getActions(clazz.getSuperclass()));
            }

            // add to the cache
            synchronized (actionCache) {
                actionCache.put(clazz, out);
            }
        }

        return out;
    }

    /**
     * A sender that sends data to clients of a particular type.  The sender
     * itself is serializable, but the data it uses (the channel and session
     * set) are ManagedObjects.  Therefore any number of separate senders
     * can point to the same underlying session set and channels.
     * <p>
     * A sender is valid as long as the handler for the type it represents
     * is registered.  If the handler is unregistered, the various methods
     * may throw exceptions or have otherwise undefined behaviors.
     */
    static class WonderlandClientSenderImpl
            implements WonderlandClientSender, Serializable
    {
        /** the client type */
        private ConnectionType type;
       
        /** the underlying channel to send to all sessions */
        private ManagedReference<Channel> channelRef;
       
        /** the client ID for this client */
        private short clientID;
       
        /**
         * Create a new WonderlandClientChannelImpl
         * @param type the client type
         * @param clientID the client ID to send with
         * @param sessions the set of sessions associated with this sender
         * @param channel the channel to wrap
         */
        public WonderlandClientSenderImpl(ConnectionType type, short clientID,
                                          ClientSessionSet sessions,
                                          Channel channel)
        {
            this.type     = type;
            this.clientID = clientID;
       
            // create references
            DataManager dm = AppContext.getDataManager();
            channelRef     = dm.createReference(channel);
        }
       
        public ConnectionType getClientType() {
            return type;
        }

        public Set<WonderlandClientID> getClients() {
            DataManager dm = AppContext.getDataManager();
            Set<WonderlandClientID> out = new LinkedHashSet<WonderlandClientID>();

            for (Iterator<ClientSession> i = channelRef.get().getSessions();
                 i.hasNext();)
            {
                ManagedReference<ClientSession> ref = dm.createReference(i.next());
                out.add(new WonderlandClientID(ref));
            }
           
            return out;
        }

        public boolean hasSessions() {
            return channelRef.get().hasSessions();
        }

        public void send(Message message) {
            send(channelRef.get(), message);
        }

        public void send(WonderlandClientID wlID, Message message) {
            // issue 963: session may be null
            ClientSession session = wlID.getSession();
            if (session != null) {
                session.send(serializeMessage(message, clientID));
            }
        }

        public void send(Set<WonderlandClientID> wlIDs, Message message)
        {
            // send to each individual session
            for (WonderlandClientID wlID : wlIDs) {
                send(wlID, message);
            }
        }

        public void send(Channel channel, Message message) {
            channel.send(null, serializeMessage(message, clientID));
        }
       
        /**
         * Get the clientID for this sender
         * @return the client ID
         */
        private short getClientID() {
            return clientID;
        }
       
        /**
         * Add a new session to this sender.  This affects the global state
         * of all senders of this type.
         * @param session the session to add
         */
        private void addSession(ClientSession session) {
            channelRef.get().join(session);
        }
       
        /**
         * Remove a session from this sender.  This affects the global state
         * of all senders of this type.  Note that there is no need to call
         * this on disconnect, since Darkstar will automatically remove
         * the session from the channel.
         * @param session the session to remove
         */
        private void removeSession(ClientSession session) {
            channelRef.get().leave(session);
        }
    }
     
    /**
     * Handle internal messages from the WonderlandSession object
     */
    static class SessionInternalConnectionHandler implements ClientConnectionHandler, Serializable {
        /** the listener to call back to */
        private ManagedReference<WonderlandSessionListener> listener;
       
        /**
         * Set the session listener
         * @param listener the session listener
         */
        public void setListener(WonderlandSessionListener listener) {
            this.listener = AppContext.getDataManager().createReference(listener);
        }
       
        public ConnectionType getConnectionType() {
            return SessionInternalConnectionType.SESSION_INTERNAL_CLIENT_TYPE;
        }

        public void registered(WonderlandClientSender sender) {
            // ignore
        }
       
        public void clientConnected(WonderlandClientSender sender,
                                    WonderlandClientID clientID,
                                    Properties properties)
        {
            // ignore
        }
       
        public void clientDisconnected(WonderlandClientSender sender,
                                       WonderlandClientID clientID)
        {
            // ignore
        }

        public void messageReceived(WonderlandClientSender sender,
                                    WonderlandClientID clientID,
                                    Message message)
        {
            if (message instanceof AttachClientMessage) {
                AttachClientMessage acm = (AttachClientMessage) message;
                listener.get().handleAttach(acm.getMessageID(),
                                            acm.getClientType(),
                                            acm.getProperties());
            } else if (message instanceof DetachClientMessage) {
                DetachClientMessage dcm = (DetachClientMessage) message;
                listener.get().handleDetach(dcm.getClientID(), false);
            }
        }
    }
   
    /**
     * Store all registered handlers, mapped by ConnectionType
     */
    static class HandlerStore implements ManagedObject, Serializable {
        /** the key in the data store */
        private static final String DS_KEY = HandlerStore.class.getName();
       
        /** the handlers, mapped by ConnectionType */
        private final Map<ConnectionType, HandlerRecord> handlers =
                new HashMap<ConnectionType, HandlerRecord>();
       
        /** The next client ID to assign */
        private short clientID = 0;
       
        /**
         * Register a new handler type
         * @param handler the handler to register
         * @return the clientID that will be used for this handler
         */
        public short register(ClientConnectionHandler handler) {
           
            // check for duplicates
            if (handlers.containsKey(handler.getConnectionType())) {
                throw new IllegalStateException("Handler for type " +
                        handler.getConnectionType() + " already registered.");
            }
           
            // decide the correct type of reference depending on if the
            // handler is a managed object or not
            ClientHandlerRef ref;
            if (handler instanceof ManagedObject) {
                ref = new ManagedClientHandlerRef(handler);
            } else {
                ref = new ClientHandlerRef(handler);
            }
           
            // figure out the client ID to assign to this handler
            short assignID = this.clientID++;
            if (handler instanceof SessionInternalConnectionHandler) {
                // special case -- force ID of internal handler
                assignID = SessionInternalConnectionType.SESSION_INTERNAL_CLIENT_ID;
            }
           
            // create a ClientSessionSet and channel
            DataManager dm = AppContext.getDataManager();
            ChannelManager cm = AppContext.getChannelManager();
           
            ClientSessionSet sessions = new ClientSessionSet();
            String channelName = handler.getConnectionType().toString() +
                                 "(" + String.valueOf(assignID) + ")";
            Channel channel = cm.createChannel(channelName,
                                               null,
                                               Delivery.RELIABLE);

            // mark ourself for udate
            dm.markForUpdate(this);

            // add to the map
            HandlerRecord record = new HandlerRecord();
            record.ref = ref;
            record.channel  = dm.createReference(channel);
            record.sessions = dm.createReference(sessions);
            record.clientID = assignID;
            handlers.put(handler.getConnectionType(), record);
           
            return record.clientID;
        }
       
        /**
         * Unregister a handler
         * @param handler the handler to unregister
         */
        public void unregister(ClientConnectionHandler handler) {
            // mark ourself for udate
            DataManager dm = AppContext.getDataManager();
            dm.markForUpdate(this);

            HandlerRecord record = handlers.remove(handler.getConnectionType());
 
            // remove the channel and session store
            dm.removeObject(record.channel.get());
            dm.removeObject(record.sessions.get());
           
            // clear the reference, which will remove a managed object
            // handler from the data store
            if (record != null) {
                record.ref.clear();
            }
        }
       
        /**
         * Get all registered handlers
         * @return the set of all registered handlers
         */
        public Set<ClientConnectionHandler> getHandlers() {
            Set<ClientConnectionHandler> out =
                    new HashSet<ClientConnectionHandler>(handlers.size());
           
            for (HandlerRecord record : handlers.values()) {
                out.add(record.ref.get());
            }
           
            return out;
        }
       
        /**
         * Get a handler for the given client type
         * @param type the client type to look up a handler for
         * @return a handler for the given type, or null if none is
         * registered
         */
        public ClientHandlerRef getHandlerRef(ConnectionType type) {
            HandlerRecord record = handlers.get(type);
            if (record == null) {
                return null;
            }
           
            return record.ref;
        }
       
        /**
         * Get the sender for the given client type
         * @param type the client type to get a sender for
         * @return a sender for the given type, or null if the type is
         * not registered
         */
        public WonderlandClientSenderImpl getSender(ConnectionType type) {
            HandlerRecord record = handlers.get(type);
            if (record == null) {
                return null;
            }
           
            return new WonderlandClientSenderImpl(type, record.clientID,
                                record.sessions.get(), record.channel.get());
        }
       
        // a handler reference and its associated channel
        class HandlerRecord implements Serializable {
            ClientHandlerRef ref;
            ManagedReference<Channel> channel;
            ManagedReference<ClientSessionSet> sessions;
            short clientID;
        }
    }
   
    /**
     * A reference to a regular client handler
     */
    static class ClientHandlerRef implements Serializable {
        private ClientConnectionHandler handler;
      
        public ClientHandlerRef(ClientConnectionHandler handler) {
            this.handler = handler;
        }
       
        public ClientConnectionHandler get() {
            return handler;
        }
       
        public void clear() {
            handler = null;
        }
    }
   
    /**
     * A reference to a managed client handler
     */
    static class ManagedClientHandlerRef extends ClientHandlerRef
            implements Serializable
    {
        private ManagedReference<ClientConnectionHandler> ref;
       
        public ManagedClientHandlerRef(ClientConnectionHandler handler) {
            super (null);
           
            DataManager dm = AppContext.getDataManager();
            ref = dm.createReference(handler);
        }
       
        @Override
        public ClientConnectionHandler get() {
            return ref.get();
        }
       
        @Override
        public void clear() {
            ClientConnectionHandler handler = get();
            AppContext.getDataManager().removeObject((ManagedObject) handler);
        }
    }
   
    /**
     * A Set of client sessions
     */
    static class ClientSessionSet extends HashSet<ManagedReference<ClientSession>>
            implements ManagedObject
    {
    }

    static class ReceiveSecureTask implements SecureTask, Serializable {
        private String resourceID;
        private short clientID;
        private Set<Action> actions;
        private Message message;
        private String bindingName;

        public ReceiveSecureTask(String resourceID, short clientID,
                                 Set<Action> actions, Message message,
                                 String bindingName)
        {
            this.resourceID = resourceID;
            this.clientID = clientID;
            this.actions = actions;
            this.message = message;
            this.bindingName = bindingName;
        }

        public void run(ResourceMap granted) {
            ActionMap am = granted.get(resourceID);

            // make sure all actions were granted
            Set<Action> grantedActions = new HashSet<Action>(am.values());

            // get the binding for the session listener
            WonderlandSessionListener listener = (WonderlandSessionListener)
                    AppContext.getDataManager().getBinding(bindingName);

            // use the client ID to get the values we need to pass in
            // to the handler
            SecureClientConnectionHandler handler =
                    (SecureClientConnectionHandler) listener.getHandler(clientID);
            WonderlandClientSender sender = listener.senders.get(clientID);
            WonderlandClientID wID = listener.getWonderlandClientID();

            // test if the request was granted -- we do this by simply
            // comparing the size of the requested actions set to the
            // size of the granted action set, since we don't care
            // which specific actions were granted or not
            if (actions.size() == grantedActions.size()) {
                // request was accepted -- continue processing
                handler.messageReceived(sender, wID, message);
            } else {
                // the message was rejected -- notify the handler
                logger.fine("Session " + listener.getSession().getName() +
                        " permission denied for message " + message);

                if (handler.messageRejected(sender, wID, message, actions,
                        grantedActions)) {
                    // the handler isn't sending an error, so we need to
                    listener.sendError(message.getMessageID(), clientID,
                            "Permission denied.");
                }
            }
        }
    }

    private static class AttachSecureTask implements SecureTask, Serializable {
        private String resourceID;
        private MessageID messageID;
        private ConnectionType type;
        private Properties properties;
        private ClientHandlerRef ref;
        private String bindingName;

        public AttachSecureTask(String resourceID, MessageID messageID,
                                ConnectionType type, Properties properties,
                                ClientHandlerRef ref, String bindingName)
        {
            this.resourceID = resourceID;
            this.messageID = messageID;
            this.type = type;
            this.properties = properties;
            this.ref = ref;
            this.bindingName = bindingName;
        }

        public void run(ResourceMap granted) {
            ActionMap am = granted.get(resourceID);

            // get the binding for the session listener
            WonderlandSessionListener listener = (WonderlandSessionListener)
                    AppContext.getDataManager().getBinding(bindingName);

            if (am.containsKey(ConnectAction.getInstance().getName())) {
                // request was accepted -- continue processing
                listener.finishAttach(messageID, type, properties, ref);
            } else {
                logger.fine("Session " + listener.getSession().getName() +
                        " permission denied for client type " + type);

                // notify the handler of the rejection
                SecureClientConnectionHandler handler =
                        (SecureClientConnectionHandler) ref.get();
                WonderlandClientID clientID = listener.getWonderlandClientID();
                handler.connectionRejected(clientID);

                // send an error back to the sender
                listener.sendError(messageID, SESSION_INTERNAL_CLIENT_ID,
                        "Permission denied for " + type);
            }
        }
    }

    private static class NotifyDisconnectTask implements Task, Serializable {
        private WonderlandClientID clientID;
        private ClientHandlerRef handlerRef;
        private WonderlandClientSender sender;

        public NotifyDisconnectTask(WonderlandClientID clientID,
                                    ClientHandlerRef handlerRef,
                                    WonderlandClientSender sender)
        {
            this.clientID = clientID;
            this.handlerRef = handlerRef;
            this.sender = sender;
        }

        public void run() throws Exception {
            handlerRef.get().clientDisconnected(sender, clientID);
        }
    }
}
TOP

Related Classes of org.jdesktop.wonderland.server.comms.WonderlandSessionListener

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.