Package org.coweb

Source Code of org.coweb.LateJoinHandler$BatchUpdateMessage

package org.coweb;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.logging.Logger;

import org.cometd.bayeux.Message;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.bayeux.server.LocalSession;
import org.eclipse.jetty.util.ajax.JSON;

//import org.coweb.LateJoinHandler.BatchUpdateMessage;

public class LateJoinHandler {

  protected SessionHandler sessionHandler = null;
  protected ServiceHandler serviceHandler = null;
  protected SessionManager sessionManager = null;
  protected SessionModerator sessionModerator = null;
  protected UpdaterTypeMatcher updaterTypeMatcher = null;
  protected boolean cacheState = true;

  private static final Logger log = Logger.getLogger(LateJoinHandler.class
      .getName());

  private Map<String, ServerSession> updatees =
        new HashMap<String, ServerSession>();

  protected Map<String, List<String>> updaters =
        new HashMap<String, List<String>>();

  /**
   * List of available siteids. An index with a null value is an available
   * siteid, otherwise the slot is filled with ServerSession's clientid (i.e.
   * {@link org.cometd.bayeux.Session#getId}).
   */
  protected ArrayList<String> siteids = new ArrayList<String>(5);

  private Object[] lastState = null;

  /**
   * Map of bayeux client id to ServerSession. "clientId" is defined as the
     * return value of {@link org.cometd.bayeux.Session#getId}. For
     * LocalSession/ServerSession pairs, the values are identical (e.g. for the
     * SessionModerator Session pair).
   */
  private Map<String, ServerSession> clientids =
        new HashMap<String, ServerSession>();

  public LateJoinHandler(SessionHandler sessionHandler,
      Map<String, Object> config) {

    this.siteids.add(0, "reserved");
    for (int i = 1; i < 5; i++) {
      this.siteids.add(i, null);
    }

    this.sessionHandler = sessionHandler;
    this.serviceHandler = this.sessionHandler.getServiceHandler();
    this.sessionManager = SessionManager.getInstance();

    if (config.containsKey("cacheState")) {
      this.cacheState = ((Boolean)config.get("cacheState"));
    }

    String classStr = "org.coweb.DefaultUpdaterTypeMatcher";
    if (config.containsKey("updaterTypeMatcher")) {
      classStr = (String) config.get("updaterTypeMatcher");
    }

    try {
      Class<? extends UpdaterTypeMatcher> c = Class.forName(classStr)
          .asSubclass(UpdaterTypeMatcher.class);
      this.updaterTypeMatcher = c.newInstance();
    } catch (Exception e) {
      e.printStackTrace();
    }
   
    this.sessionModerator = sessionHandler.getSessionModerator();
    ServerSession moderator = this.sessionModerator.getServerSession();

    // make sure the moderator has joined the conference and has a site
    // id before anyone joins.  User slot 0 for moderator.
    this.siteids.set(0, moderator.getId());
    this.sessionModerator.setSessionAttribute("siteid", new Integer(0));
    this.clientids.put(moderator.getId(), moderator);
  }

  public ServerSession getServerSessionFromSiteid(String siteStr) {
    try {
      int siteid = Integer.valueOf(siteStr);
      String clientId = this.siteids.get(siteid);

      if (clientId == null)
        return null;

      return this.clientids.get(clientId);
    } catch (Exception e) {
      ;
    }

    return null;
  }

  /**
   * Clears the internal cache. Typically called when the server receives
   * a sync event, thus invalidating the cache.
   */
  public void clearCacheState() {
    this.lastState = null;
  }

  /**
   * Called when a client first attempts to join a session. This is called
     * after a client has subscribed to
     * /service/session/join/[siteid,roster,state]. This method then sends
     * siteid and roster info back to the client. If there is state to send
     * (either no updaters, so empty state, or cached state), this method sends
     * it immediately. Otherwise, this method asks an updater for state.
   *
   * @param client Remote client joining the session.
   * @param message
   * @return Whether or or not this was the first client to join.
   */
  public boolean onClientJoin(ServerSession client, Message message) {
    boolean first = false;
    int siteId = this.getSiteForClient(client);
    log.info("client site id = " + siteId);

    if (siteId == -1) {
      siteId = this.addSiteForClient(client);
      log.info("new site id = " + siteId);
    }

    Map<Integer, String> roster = this.getRosterList(client);
    Object[] data = new Object[0];

    boolean sendState = false;

    Map<String, Object> ext = message.getExt();
    @SuppressWarnings("unchecked")
    Map<String, Object> cobwebData = (Map<String, Object>) ext.get("coweb");
    String updaterType = (String) cobwebData.get("updaterType");
    client.setAttribute("updaterType", updaterType);
    log.info("updaterType = " + updaterType);

    if (this.updaters.isEmpty()) {
      /* This is the first client, since there are no other updaters. Send
             * empty state and make this client an updater for future late
             * joiners. */
      this.addUpdater(client, false);
      sendState = true;
      first = true;
    } else if (this.lastState == null) {
      /* Assign another client to be the "updater" of the newly joined
             * client. */
      this.assignUpdater(client, updaterType);
      sendState = false;
    } else {
      /* Send the cached state, but don't make this client an updater. */
      data = this.lastState;
      sendState = true;
    }

    client.batch(new BatchUpdateMessage(client, siteId, roster, data,
          sendState));
    return first;
  }

  /**
   * Called when some updater client sends its full state to the server.
   * This method checks the updater token to determine which newly joined
   * client should receive the full application state.
   * @param client The updater client.
   * @param message Contains token to identify the late joiner to get the
     *                state.
   */
  public void onUpdaterSendState(ServerSession client, Message message) {
    //log.info(message.getJSON());
    String clientId = client.getId();
    Map<String, Object> data = message.getDataAsMap();

    String token = (String) data.get("token");
    if (token == null) {
      /* Malicious or buggy client. */
      this.sessionHandler.removeBadClient(client);
      return;
    }

    List<String> tokens = this.updaters.get(clientId);
    if (tokens == null) {
      /* Client isn't really an updater: possible malicious or buggy
             * client. */
      this.sessionHandler.removeBadClient(client);
      return;
    }

    if (!tokens.remove(token)) {
      /* Token not found: again, client is possibly malicious or buggy. */
      this.sessionHandler.removeBadClient(client);
      return;
    }

    /* Now, we have a validated token. Find the updatee. */
    ServerSession updatee = this.updatees.get(token);
    if (updatee == null)
      return;

    this.updatees.remove(token);
    /* Should we cache the state? */
    if (this.cacheState) {
      this.lastState = (Object[]) data.get("state");
      log.fine("Cached state from updater.");
      log.fine(JSON.toString(this.lastState));
    }

    ServerMessage.Mutable msg = this.sessionManager.getBayeux()
        .newMessage();

    msg.setChannel("/service/session/join/state");
    if (this.cacheState) {
      log.info("sending cached state");
      msg.setData(this.lastState);
    } else {
      log.info("sending state from updater");
      msg.setData((Object[]) data.get("state"));
    }
    msg.setLazy(false);

    updatee.deliver(this.sessionManager.getServerSession(), msg);
  }

  /**
   * Called when a client becomes an updater.
   * @param client The new updater.
   * @param message
   */
  public void onUpdaterSubscribe(ServerSession client, Message message) {
    this.addUpdater(client, true);
  }

  /**
   * Called when a client leaves a session.
   * @param client The client who is leaving.
   * @return True if and only if this was the last updater.
   */
  public boolean onClientRemove(ServerSession client) {

    log.info("siteId = " + client.getAttribute("siteid"));

    this.removeUpdater(client);
    if (this.getUpdaterCount() == 0) {
      log.info("removing last updater, ending coweb session");
      return true;
    }

    return false;
  }

  /**
   * Called when all clients leave a session.
   */
  public boolean onEndSession() {
    this.updatees.clear();
    this.updaters.clear();
    this.siteids.clear();
    this.lastState = null;
    this.clientids.clear();

    return true;
  }

  /**
   * Called when a client becomes an updated (typically when a new client
     * joins a session).
   * @param serverSession
   * @param notify Whether or not to send the updated roster information.
   */
  protected void addUpdater(ServerSession serverSession, boolean notify) {
    String clientId = serverSession.getId();

    /* TODO ??? The comment below and the if statement contradict each
         * other.
     * Moreover, the !this.updaters.isEmpty() is redundant... */
    // check if this client is already an updater and ignore unless this is
    // the first updater
    if (this.updaters.containsKey(clientId) && !this.updaters.isEmpty()) {
      return;
    }

    // serverSession.setAttribute("username", clientId);
    log.fine("adding " + clientId + " to list of updaters");
    this.updaters.put(clientId, new ArrayList<String>());

    if (notify) {
      this.sendRosterAvailable(serverSession);
    }
  }

  /**
   * Let all clients know that another client has been added to the roster.
   * @param client The new client.
   */
  private void sendRosterAvailable(ServerSession client) {
    log.info("sending roster available");
    ServerSession from = this.sessionManager.getServerSession();

    Integer siteId = (Integer) client.getAttribute("siteid");
    String username = (String) client.getAttribute("username");

    Map<String, Object> data = new HashMap<String, Object>();
    data.put("siteId", siteId);
    data.put("username", username);

    String rosterAvailableChannel =
            this.sessionHandler.getRosterAvailableChannel();
    for (ServerSession c : this.sessionHandler.getAttendees()) {
      c.deliver(from, rosterAvailableChannel, data, null);
    }

  }

  /**
   * Let all clients know some client left the roster.
   * @param client The old client.
   */
  protected void sendRosterUnavailable(ServerSession client) {
    log.fine("CollabSessionHandler::sendRosterUnavailable");
    /* create channel */
        BayeuxServer server = this.sessionManager.getBayeux();
        ServerChannel.Initializer initializer =
            new ServerChannel.Initializer() {
      @Override
      public void configureChannel(ConfigurableServerChannel channel) {
        channel.setPersistent(true);
      }
    };

    String rosterUnavailableChannel =
            this.sessionHandler.getRosterUnavailableChannel();
    server.createIfAbsent(rosterUnavailableChannel, initializer);
    ServerChannel channel = server.getChannel(rosterUnavailableChannel);
    if (channel == null) {
      return;
    }

    ServerSession from = this.sessionManager.getServerSession();

    Integer siteId = (Integer) client.getAttribute("siteid");
    String username = (String) client.getAttribute("username");

    Map<String, Object> data = new HashMap<String, Object>();
    data.put("siteId", siteId);
    data.put("username", username);

    log.fine(data.toString());

    channel.publish(from, data, null);
  }

  public String toString() {
    return "LateJoinHandler";
  }

  /**
   * Finds the site id for a client. Returns -1 if none exists.
   * @param client
   * @return Site id or -1 if nonexistent.
   */
  protected int getSiteForClient(ServerSession client) {
    if (this.siteids.contains(client.getId())) {
      return this.siteids.indexOf(client.getId());
    }

    return -1;
  }

  /**
   * Assign site id to client.
   * @param client
   */
  protected int addSiteForClient(ServerSession client) {

    int index = this.siteids.indexOf(null);
    if (index == -1) {
      index = this.siteids.size();
      this.siteids.ensureCapacity(this.siteids.size() + 1);
      this.siteids.add(index, client.getId());
    } else
      this.siteids.set(index, client.getId());

    client.setAttribute("siteid", new Integer(index));
    this.clientids.put(client.getId(), client);

    return index;
  }

  /**
   * Remote site id for client.
   * @param client
   */
  protected int removeSiteForClient(ServerSession client) {

    if (client == null) {
      log.severe("removeSiteForClient ******* client is null *******");
      return -1;
    }

    int siteid = this.siteids.indexOf(client.getId());
    if (siteid == -1) {
      log.severe("removeSiteForClient ****** Cannot find client " +
                    "in siteids list *******");
      Integer i = (Integer) client.getAttribute("siteid");
      if (i == null) {
        log.severe("******* Client Does not have siteId attribute " +
                        "- Ghost *******");
      }
      return -1;
    }

    this.siteids.set(siteid, null);
    return siteid;
  }

  /**
   * Create a new roster list map; this will be sent to joining clients.
   * @param client New client.
   */
  protected Map<Integer, String> getRosterList(ServerSession client) {

    Map<Integer, String> roster = new HashMap<Integer, String>();

    for (String clientId : this.updaters.keySet()) {
      ServerSession c = this.clientids.get(clientId);
      Integer siteId = (Integer) c.getAttribute("siteid");
      roster.put(siteId, (String) c.getAttribute("username"));
    }

    return roster;
  }

  /**
   * Assign an updater to provide full application state to a newly joined
   * client.
   * @param updatee The newly joined client.
   * @param updaterType Updater type as specified by the OCW application.
   */
  private void assignUpdater(ServerSession updatee, String updaterType) {
    log.info("assignUpdater *****************");
    ServerSession from = this.sessionManager.getServerSession();
    if (this.updaters.isEmpty()) {
      this.addUpdater(updatee, false);
      ((ServerSession)updatee).deliver(from, "/service/session/join/state",
          new ArrayList<String>(), null);
      return;
    }

    String updaterId = null;
    ServerSession updater = null;
    if (!updaterType.equals("default")) {
      /* Try to find a custom updater. */
      String matchedType = updaterTypeMatcher.match(updaterType,
          getAvailableUpdaterTypes());
      if (matchedType != null) {
        for (String id : this.updaters.keySet()) {
          updater = this.clientids.get(id);
          if (updater.getAttribute("updaterType").equals(matchedType)) {
            updaterId = id;
            log.fine("found an updater type matched to [" + matchedType + "]");
            break;
          }
        }
      }
    }
    if (updaterId == null) {
      /* Choose random updater for default types or if a custom updater wasn't
       * found. */
      Random r = new Random();
      int idx = r.nextInt(this.updaters.size());

      log.fine("using default updater type");
      Object[] keys = this.updaters.keySet().toArray();
      updaterId = (String) keys[idx];
      updater = this.clientids.get(updaterId);
    }

    /* Now we've found an updater, so generate a token and ask the updater to
     * provide full application state. */
    log.fine("assigning updater " + updater.getAttribute("siteid") + " to "
        + updatee.getAttribute("siteid"));
    SecureRandom s = new SecureRandom();
    String token = new BigInteger(130, s).toString(32);

    this.updaters.get(updaterId).add(token);
    this.updatees.put(token, updatee);

    updater.deliver(from, "/service/session/updater", token, null);
  }

  /**
   * Called when a client is no longer an updater (typically when the client
   * disconnects from the session).
   * @param client
   */
  protected void removeUpdater(ServerSession client) {
    log.fine("LateJoinHandler::removeUpdater " + client);
    this.removeSiteForClient(client);

    List<String> tokenList = this.updaters.get(client.getId());
    this.updaters.remove(client.getId());
    if (tokenList == null) {
      for (String token : this.updatees.keySet()) {
        ServerSession updatee = this.updatees.get(token);
        if (updatee.getId().equals(client.getId())) {
          this.updatees.remove(token);
        }
      }
    } else {
      log.fine("sending roster unavailable");
      this.sendRosterUnavailable(client);
      if (!tokenList.isEmpty()) {
        log.fine("this updater was updating someone");
        for (String token : tokenList) {
          ServerSession updatee = this.updatees.get(token);
          if (updatee == null)
            continue;

          // this.updatees.remove(token);
          String updaterType = (String) client
              .getAttribute("updaterType");
          if (updaterType == null) {
            updaterType = "default";
          }
          this.assignUpdater(updatee, updaterType);
        }
      }
    }
  }

  private int getUpdaterCount() {
    return this.updaters.size();
  }

  private List<String> getAvailableUpdaterTypes() {
    List<String> availableUpdaterTypes = new ArrayList<String>();
    for (String id : this.updaters.keySet()) {
      ServerSession updater = this.clientids.get(id);
      availableUpdaterTypes.add((String) updater
          .getAttribute("updaterType"));
    }
    return availableUpdaterTypes;
  }

  /**
   * Used to send join state and other important session data to joining
     * clients. This class gives the work to a separate thread so that the
     * server can continue to process other requsts.
   */
  class BatchUpdateMessage implements Runnable {

    private ServerSession client = null;
    private Object data = null;
    private Map<Integer, String> roster = null;
    private int siteId = -1;
    private boolean sendState = false;

    /**
      *
      * Sends all the important information to a client upon joining.
      * The data parameter should be an Object[] with three elements:
      *   <ul>
      *     <li> [{topic: coweb.state.set.collab_name, value: application
          *     state}, ...] (this will be an array)
      *     <li> {topic: coweb.engine.state, value: TODO}
      *     <li> {topic: coweb.pause.state, value: TODO}
      *   </ul>
      *
      * @param siteId site id
      * @param roster current session roster
      * @param data session state
      * @param sendState whether or not to send session state
      */
    BatchUpdateMessage(ServerSession client, int siteId,
                Map<Integer, String> roster, Object data, boolean sendState) {
      this.client = client;
      this.siteId = siteId;
      this.roster = roster;
      this.data = data;
      this.sendState = sendState;
    }

    @Override
    public void run() {
      SessionManager manager = SessionManager.getInstance();
      ServerSession server = manager.getServerSession();

      this.client.deliver(server, "/service/session/join/siteid",
          this.siteId, null);
      this.client.deliver(server, "/service/session/join/roster",
          this.roster, null);

      if (this.sendState) {
        this.client.deliver(server, "/service/session/join/state",
            this.data, null);
      }
    }
  }

}
TOP

Related Classes of org.coweb.LateJoinHandler$BatchUpdateMessage

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.