Package net.socialgamer.cah.data

Source Code of net.socialgamer.cah.data.ConnectedUsers

/**
* Copyright (c) 2012, Andy Janata
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
* provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of conditions
*   and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of
*   conditions and the following disclaimer in the documentation and/or other materials provided
*   with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package net.socialgamer.cah.data;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;

import net.socialgamer.cah.Constants.DisconnectReason;
import net.socialgamer.cah.Constants.ErrorCode;
import net.socialgamer.cah.Constants.LongPollEvent;
import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.data.QueuedMessage.MessageType;

import org.apache.log4j.Logger;

import com.google.inject.Singleton;


/**
* Class that holds all users connected to the server, and provides functions to operate on said
* list.
*
* @author Andy Janata (ajanata@socialgamer.net)
*/
@Singleton
public class ConnectedUsers {

  private static final Logger logger = Logger.getLogger(ConnectedUsers.class);

  /**
   * Duration of a ping timeout, in nanoseconds.
   */
  public static final long PING_TIMEOUT = TimeUnit.SECONDS.toNanos(90);

  /**
   * Duration of an idle timeout, in nanoseconds.
   */
  public static final long IDLE_TIMEOUT = TimeUnit.MINUTES.toNanos(60);

  /**
   * Key (username) must be stored in lower-case to facilitate case-insensitivity in nicks.
   */
  private final Map<String, User> users = new HashMap<String, User>();

  /**
   * @param userName
   *          User name to check.
   * @return True if {@code userName} is a connected user.
   */
  public boolean hasUser(final String userName) {
    return users.containsKey(userName.toLowerCase());
  }

  /**
   * Checks to see if the specified {@code user} is allowed to connect, and if so, add the user,
   * as an atomic operation.
   * @param user User to add. {@code getNickname()} is used to determine the nickname.
   * @param maxUsers Maximum number of users allowed to connect. Admins are always allowed to
   * connect.
   * @return {@code null} if the user was added, or an {@link ErrorCode} explaining why the user was
   * rejected.
   */
  public ErrorCode checkAndAdd(final User user, final int maxUsers) {
    synchronized (users) {
      if (this.hasUser(user.getNickname())) {
        logger.info(String.format("Rejecting existing username %s from %s", user.toString(),
            user.getHostName()));
        return ErrorCode.NICK_IN_USE;
      } else if (users.size() >= maxUsers && !user.isAdmin()) {
        logger.warn(String.format("Rejecting user %s due to too many users (%d >= %d)",
            user.toString(), users.size(), maxUsers));
        return ErrorCode.TOO_MANY_USERS;
      } else {
        logger.info(String.format("New user %s from %s (admin=%b)", user.toString(),
            user.getHostName(), user.isAdmin()));
        users.put(user.getNickname().toLowerCase(), user);
        final HashMap<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
        data.put(LongPollResponse.EVENT, LongPollEvent.NEW_PLAYER.toString());
        data.put(LongPollResponse.NICKNAME, user.getNickname());
        broadcastToAll(MessageType.PLAYER_EVENT, data);
        return null;
      }
    }
  }

  /**
   * Remove a user from the user list, and mark them as invalid so the next time they make a request
   * they can be informed.
   *
   * @param user
   *          User to remove.
   * @param reason
   *          Reason the user is being removed.
   */
  public void removeUser(final User user, final DisconnectReason reason) {
    synchronized (users) {
      if (users.containsValue(user)) {
        logger.info(String.format("Removing user %s because %s", user.toString(), reason));
        user.noLongerVaild();
        users.remove(user.getNickname().toLowerCase());
        notifyRemoveUser(user, reason);
      }
    }
  }

  /**
   * Get the User for the specified nickname, or null if no such user exists.
   *
   * @param nickname
   * @return User, or null.
   */
  @Nullable
  public User getUser(final String nickname) {
    return users.get(nickname.toLowerCase());
  }

  /**
   * Broadcast to all remaining users that a user has left.
   *
   * @param user
   *          User that has left.
   * @param reason
   *          Reason why the user has left.
   */
  private void notifyRemoveUser(final User user, final DisconnectReason reason) {
    // Games are informed about the user leaving when the user object is marked invalid.
    final HashMap<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
    data.put(LongPollResponse.EVENT, LongPollEvent.PLAYER_LEAVE.toString());
    data.put(LongPollResponse.NICKNAME, user.getNickname());
    data.put(LongPollResponse.REASON, reason.toString());
    broadcastToAll(MessageType.PLAYER_EVENT, data);
  }

  /**
   * Check for any users that have not communicated with the server within the ping timeout delay,
   * and remove users which have not so communicated. Also remove clients which are still connected,
   * but have not actually done anything for a long time.
   */
  public void checkForPingAndIdleTimeouts() {
    final Map<User, DisconnectReason> removedUsers = new HashMap<User, DisconnectReason>();
    synchronized (users) {
      final Iterator<User> iterator = users.values().iterator();
      while (iterator.hasNext()) {
        final User u = iterator.next();
        DisconnectReason reason = null;
        if (System.nanoTime() - u.getLastHeardFrom() > PING_TIMEOUT) {
          reason = DisconnectReason.PING_TIMEOUT;
        }
        else if (!u.isAdmin() && System.nanoTime() - u.getLastUserAction() > IDLE_TIMEOUT) {
          reason = DisconnectReason.IDLE_TIMEOUT;
        }
        if (null != reason) {
          removedUsers.put(u, reason);
          iterator.remove();
        }
      }
    }
    // Do this later to not keep users locked
    for (final Entry<User, DisconnectReason> entry : removedUsers.entrySet()) {
      try {
        entry.getKey().noLongerVaild();
        notifyRemoveUser(entry.getKey(), entry.getValue());
        logger.info(String.format("Automatically kicking user %s due to %s", entry.getKey(),
            entry.getValue()));
      } catch (final Exception e) {
        logger.error("Unable to remove pinged-out user", e);
      }
    }
  }

  /**
   * Broadcast a message to all connected players.
   *
   * @param type
   *          Type of message to broadcast. This determines the order the messages are returned by
   *          priority.
   * @param masterData
   *          Message data to broadcast.
   */
  public void broadcastToAll(final MessageType type,
      final HashMap<ReturnableData, Object> masterData) {
    broadcastToList(users.values(), type, masterData);
  }

  /**
   * Broadcast a message to a specified subset of connected players.
   *
   * @param broadcastTo
   *          List of users to broadcast the message to.
   * @param type
   *          Type of message to broadcast. This determines the order the messages are returned by
   *          priority.
   * @param masterData
   *          Message data to broadcast.
   */
  public void broadcastToList(final Collection<User> broadcastTo, final MessageType type,
      final HashMap<ReturnableData, Object> masterData) {
    // TODO I think this synchronized block is pointless.
    synchronized (users) {
      for (final User u : broadcastTo) {
        @SuppressWarnings("unchecked")
        final Map<ReturnableData, Object> data = (Map<ReturnableData, Object>) masterData.clone();
        data.put(LongPollResponse.TIMESTAMP, System.currentTimeMillis());
        final QueuedMessage qm = new QueuedMessage(type, data);
        u.enqueueMessage(qm);
      }
    }
  }

  /**
   * @return A copy of the list of connected users.
   */
  public Collection<User> getUsers() {
    synchronized (users) {
      return new ArrayList<User>(users.values());
    }
  }
}
TOP

Related Classes of net.socialgamer.cah.data.ConnectedUsers

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.