content: /*
   * Copyright (C) 2013 Glyptodon LLC
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to deal
   * in the Software without restriction, including without limitation the rights
   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   * copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
   * THE SOFTWARE.
   */
  
  package net.sourceforge.guacamole.net.auth.mysql;
  
  
  import com.google.inject.Inject;
  import java.util.Collection;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.Map;
  import org.glyptodon.guacamole.GuacamoleException;
  import net.sourceforge.guacamole.net.auth.mysql.dao.ConnectionHistoryMapper;
  import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionHistory;
  import org.glyptodon.guacamole.GuacamoleResourceNotFoundException;
  
  /**
   * Represents the map of currently active Connections to the count of the number
   * of current users. Whenever a socket is opened, the connection count should be
   * incremented, and whenever a socket is closed, the connection count should be 
   * decremented.
   *
   * @author James Muehlner
   */
  public class ActiveConnectionMap {
      
      /**
       * Represents the count of users currently using a MySQL connection.
       */
      public class Connection {
          
          /**
           * The ID of the MySQL connection that this Connection represents.
           */
          private int connectionID;
          
          /**
           * The number of users currently using this connection.
           */
          private int currentUserCount;
          
          /**
           * Returns the ID of the MySQL connection that this Connection 
           * represents.
           * 
           * @return the ID of the MySQL connection that this Connection 
           * represents.
           */
          public int getConnectionID() {
              return connectionID;
          }
          
          /**
           * Returns the number of users currently using this connection.
           * 
           * @return the number of users currently using this connection.
           */
          public int getCurrentUserCount() {
              return currentUserCount;
          }
          
          /**
           * Set the current user count for this connection.
           * 
           * @param currentUserCount The new user count for this Connection.
           */
          public void setCurrentUserCount(int currentUserCount) {
              this.currentUserCount = currentUserCount;
          }
          
          /**
           * Create a new Connection for the given connectionID with a zero
           * current user count.
           * 
           * @param connectionID The ID of the MySQL connection that this 
           *                     Connection represents.
           */
          public Connection(int connectionID) {
              this.connectionID = connectionID;
              this.currentUserCount = 0;
          }
      }
      
      /*
       * Represents a user connected to a connection or BALANCING connection group.
       */
      public class ConnectionUser {
          /**
           * The ID of the connection or connection group that this ConnectionUser refers to.
           */
          private int identifier; 
          
          /**
           * The user that this ConnectionUser refers to.
           */
          private int userID;
  
          /**
           * Returns ID of the connection or connection group that this ConnectionUser refers to.
           * @return ID of the connection or connection group that this ConnectionUser refers to.
           */
          public int getIdentifier() {
              return identifier;
          }
  
          /**
           * Returns the user ID that this ConnectionUser refers to.
           * @return the user ID that this ConnectionUser refers to.
           */
          public int getUserID() {
              return userID;
          }
          
          /**
           * Create a ConnectionUser with the given connection or connection group
           * ID and user ID.
           * 
           * @param identifier The connection or connection group ID that this 
           *                   ConnectionUser refers to.
           * @param userID The user ID that this ConnectionUser refers to.
           */
          public ConnectionUser(int identifier, int userID) {
              this.identifier = identifier;
              this.userID = userID;
          }
          
          @Override
          public boolean equals(Object other) {
              
              // Only another ConnectionUser can equal this ConnectionUser
              if(!(other instanceof ConnectionUser))
                  return false;
              
              ConnectionUser otherConnectionGroupUser = 
                      (ConnectionUser)other;
              
              /* 
               * Two ConnectionGroupUsers are equal iff they represent the exact 
               * same pairing of connection or connection group and user.
               */
              return this.identifier == otherConnectionGroupUser.identifier
                      && this.userID == otherConnectionGroupUser.userID;
          }
  
          @Override
          public int hashCode() {
              int hash = 3;
              hash = 23 * hash + this.identifier;
              hash = 23 * hash + this.userID;
              return hash;
          }
      }
  
      /**
       * DAO for accessing connection history.
       */
      @Inject
      private ConnectionHistoryMapper connectionHistoryDAO;
  
      /**
       * Map of all the connections that are currently active to the
       * count of current users.
       */
      private Map<Integer, Connection> activeConnectionMap =
              new HashMap<Integer, Connection>();
  
      /**
       * Map of all the connection group users to the count of current usages.
       */
      private Map<ConnectionUser, Integer> activeConnectionGroupUserMap =
              new HashMap<ConnectionUser, Integer>();
  
      /**
       * Map of all the connection users to the count of current usages.
       */
      private Map<ConnectionUser, Integer> activeConnectionUserMap =
              new HashMap<ConnectionUser, Integer>();
      
      /**
       * Returns the number of connections opened by the given user using 
       * the given ConnectionGroup.
       * 
       * @param connectionGroupID The connection group ID that this 
       *                          ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       * 
       * @return The number of connections opened by the given user to the given
       *         ConnectionGroup.
       */
      public int getConnectionGroupUserCount(int connectionGroupID, int userID) {
          Integer count = activeConnectionGroupUserMap.get
                  (new ConnectionUser(connectionGroupID, userID));
          
          // No ConnectionUser found means this combination was never used
          if(count == null)
              return 0;
          
          return count;
      }
      
      /**
       * Checks if the given user is currently connected to the given BALANCING
       * connection group.
       * 
       * @param connectionGroupID The connection group ID that this 
       *                          ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       * 
       * @return True if the given user is currently connected to the given 
       *         BALANCING connection group, false otherwise.
       */
      public boolean isConnectionGroupUserActive(int connectionGroupID, int userID) {
          Integer count = activeConnectionGroupUserMap.get
                  (new ConnectionUser(connectionGroupID, userID));
          
          // The connection group is in use if the ConnectionUser count > 0
          return count != null && count > 0;
      }
      
      /**
       * Increment the count of the number of connections opened by the given user
       * to the given ConnectionGroup.
       * 
       * @param connectionGroupID The connection group ID that this 
       *                          ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       */
      private void incrementConnectionGroupUserCount(int connectionGroupID, int userID) {
          int currentCount = getConnectionGroupUserCount(connectionGroupID, userID);
          
          activeConnectionGroupUserMap.put
                  (new ConnectionUser(connectionGroupID, userID), currentCount + 1);
      }
      
      /**
       * Decrement the count of the number of connections opened by the given user
       * to the given ConnectionGroup.
       * 
       * @param connectionGroupID The connection group ID that this 
       *                          ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       */
      private void decrementConnectionGroupUserCount(int connectionGroupID, int userID) {
          int currentCount = getConnectionGroupUserCount(connectionGroupID, userID);
          
          activeConnectionGroupUserMap.put
                  (new ConnectionUser(connectionGroupID, userID), currentCount - 1);
      }
      
      /**
       * Returns the number of connections opened by the given user using 
       * the given Connection.
       * 
       * @param connectionID The connection ID that this ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       * 
       * @return The number of connections opened by the given user to the given
       *         connection.
       */
      public int getConnectionUserCount(int connectionID, int userID) {
          Integer count = activeConnectionUserMap.get
                  (new ConnectionUser(connectionID, userID));
          
          // No ConnectionUser found means this combination was never used
          if(count == null)
              return 0;
          
          return count;
      }
      
      /**
       * Checks if the given user is currently connected to the given connection.
       * 
       * @param connectionID The connection ID that this ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       * 
       * @return True if the given user is currently connected to the given 
       *         connection, false otherwise.
       */
      public boolean isConnectionUserActive(int connectionID, int userID) {
          Integer count = activeConnectionUserMap.get
                  (new ConnectionUser(connectionID, userID));
          
          // The connection is in use if the ConnectionUser count > 0
          return count != null && count > 0;
      }
      
      /**
       * Increment the count of the number of connections opened by the given user
       * to the given Connection.
       * 
       * @param connectionID The connection ID that this ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       */
      private void incrementConnectionUserCount(int connectionID, int userID) {
          int currentCount = getConnectionUserCount(connectionID, userID);
          
          activeConnectionUserMap.put
                  (new ConnectionUser(connectionID, userID), currentCount + 1);
      }
      
      /**
       * Decrement the count of the number of connections opened by the given user
       * to the given Connection.
       * 
       * @param connectionID The connection ID that this ConnectionUser refers to.
       * @param userID The user ID that this ConnectionUser refers to.
       */
      private void decrementConnectionUserCount(int connectionID, int userID) {
          int currentCount = getConnectionUserCount(connectionID, userID);
          
          activeConnectionUserMap.put
                  (new ConnectionUser(connectionID, userID), currentCount - 1);
      }
      
      /**
       * Returns the ID of the connection with the lowest number of current
       * active users, if found.
       * 
       * @param connectionIDs The subset of connection IDs to find the least
       *                      used connection within.
       * 
       * @return The ID of the connection with the lowest number of current
       *         active users, if found.
       */
      public Integer getLeastUsedConnection(Collection<Integer> connectionIDs) {
          
          if(connectionIDs.isEmpty())
              return null;
          
          int minUserCount = Integer.MAX_VALUE;
          Integer minConnectionID = null;
          
          for(Integer connectionID : connectionIDs) {
              Connection connection = activeConnectionMap.get(connectionID);
              
              /*
               * If the connection is not found in the map, it has not been used,
               * and therefore will be count 0.
               */
              if(connection == null) {
                  minUserCount = 0;
                  minConnectionID = connectionID;
              }
              // If this is the least active connection
              else if(connection.getCurrentUserCount() < minUserCount) {
                  minUserCount = connection.getCurrentUserCount();
                  minConnectionID = connection.getConnectionID();
              }
          }
          
          return minConnectionID;
      }
      
      /**
       * Returns the count of currently active users for the given connectionID.
       * @return the count of currently active users for the given connectionID.
       */
      public int getCurrentUserCount(int connectionID) {
          Connection connection = activeConnectionMap.get(connectionID);
          
          if(connection == null)
              return 0;
          
          return connection.getCurrentUserCount();
      }
      
      /**
       * Decrement the current user count for this Connection.
       * 
       * @param connectionID The ID of the MySQL connection that this 
       *                     Connection represents.
       * 
       * @throws GuacamoleException If the connection is not found.
       */
      private void decrementUserCount(int connectionID)
              throws GuacamoleException {
          Connection connection = activeConnectionMap.get(connectionID);
          
          if(connection == null)
              throw new GuacamoleResourceNotFoundException
                      ("Connection to decrement does not exist.");
          
          // Decrement the current user count
          connection.setCurrentUserCount(connection.getCurrentUserCount() - 1);
      }
      
      /**
       * Increment the current user count for this Connection.
       * 
       * @param connectionID The ID of the MySQL connection that this 
       *                     Connection represents.
       * 
       * @throws GuacamoleException If the connection is not found.
       */
      private void incrementUserCount(int connectionID) {
          Connection connection = activeConnectionMap.get(connectionID);
          
          // If the Connection does not exist, it should be created
          if(connection == null) {
              connection = new Connection(connectionID);
              activeConnectionMap.put(connectionID, connection);
          }
          
          // Increment the current user count
          connection.setCurrentUserCount(connection.getCurrentUserCount() + 1);
      }
  
      /**
       * Check if a connection is currently in use.
       * @param connectionID The connection to check the status of.
       * @return true if the connection is currently in use.
       */
      public boolean isActive(int connectionID) {
          return getCurrentUserCount(connectionID) > 0;
      }
  
      /**
       * Set a connection as open.
       * @param connectionID The ID of the connection that is being opened.
       * @param userID The ID of the user who is opening the connection.
       * @param connectionGroupID The ID of the BALANCING connection group that is
       *                          being connected to; null if not used.
       * @return The ID of the history record created for this open connection.
       */
      public int openConnection(int connectionID, int userID, Integer connectionGroupID) {
  
          // Create the connection history record
          ConnectionHistory connectionHistory = new ConnectionHistory();
          connectionHistory.setConnection_id(connectionID);
          connectionHistory.setUser_id(userID);
          connectionHistory.setStart_date(new Date());
          connectionHistoryDAO.insert(connectionHistory);
  
          // Increment the user count
          incrementUserCount(connectionID);
          
          // Increment the connection user count
          incrementConnectionUserCount(connectionID, userID);
          
          // If this is a connection to a BALANCING ConnectionGroup, increment the count
          if(connectionGroupID != null)
              incrementConnectionGroupUserCount(connectionGroupID, userID);
  
          return connectionHistory.getHistory_id();
      }
  
      /**
       * Set a connection as closed.
       * @param historyID The ID of the history record about the open connection.
       * @param connectionGroupID The ID of the BALANCING connection group that is
       *                          being connected to; null if not used.
       * @throws GuacamoleException If the open connection history is not found.
       */
      public void closeConnection(int historyID, Integer connectionGroupID) 
              throws GuacamoleException {
  
          // Get the existing history record
          ConnectionHistory connectionHistory =
                  connectionHistoryDAO.selectByPrimaryKey(historyID);
  
          if(connectionHistory == null)
              throw new GuacamoleResourceNotFoundException("History record not found.");
          
          // Get the connection and user IDs
          int connectionID = connectionHistory.getConnection_id();
          int userID = connectionHistory.getUser_id();
  
          // Update the connection history record to mark that it is now closed
          connectionHistory.setEnd_date(new Date());
          connectionHistoryDAO.updateByPrimaryKey(connectionHistory);
  
          // Decrement the user count.
          decrementUserCount(connectionID);
          
          // Decrement the connection user count
          decrementConnectionUserCount(connectionID, userID);
          
          // If this is a connection to a BALANCING ConnectionGroup, decrement the count
          if(connectionGroupID != null)
              decrementConnectionGroupUserCount(connectionGroupID, userID);
      }
  }
  