/**
  * If this session is not connected, this method returns; otherwise this method sends a
  * LOGOUT_REQUEST to the server, and waits for this client to receive a LOGOUT_SUCCESS
  * acknowledgment or the timeout to expire, whichever comes first. If the LOGOUT_SUCCESS
  * acknowledgment is not received, then {@code AssertionFailedError} is thrown.
  */
 public void logout() {
   synchronized (lock) {
     if (connected == false) {
       return;
     }
     logoutAck = false;
   }
   MessageBuffer buf = new MessageBuffer(1);
   buf.putByte(SimpleSgsProtocol.LOGOUT_REQUEST);
   try {
     connection.sendBytes(buf.getBuffer());
   } catch (IOException e) {
     throw new RuntimeException(e);
   }
   synchronized (lock) {
     try {
       if (logoutAck == false) {
         lock.wait(WAIT_TIME);
       }
       if (logoutAck != true) {
         fail(toString() + " logout timed out");
       }
     } catch (InterruptedException e) {
       fail(toString() + " logout timed out: " + e.toString());
     } finally {
       if (!logoutAck) disconnect();
     }
   }
 }
  /**
   * Sends a login request and if {@code waitForLogin} is {@code true} waits for the request to be
   * acknowledged, returning {@code true} if login was successful, and {@code false} if login was
   * redirected, otherwise a {@code RuntimeException} is thrown because the login operation timed
   * out before being acknowledged.
   *
   * <p>If {@code waitForLogin} is false, this method returns {@code true} if the login is known to
   * be successful (the outcome may not yet be known because the login operation is asynchronous),
   * otherwise it returns false. Invoke {@code waitForLogin} to wait for an expected successful
   * login.
   */
  public boolean login(boolean waitForLogin) {
    synchronized (lock) {
      if (connected == false) {
        throw new RuntimeException(toString() + " not connected");
      }
    }
    String password = "******";

    MessageBuffer buf =
        new MessageBuffer(2 + MessageBuffer.getSize(name) + MessageBuffer.getSize(password));
    buf.putByte(SimpleSgsProtocol.LOGIN_REQUEST)
        .putByte(protocolVersion)
        .putString(name)
        .putString(password);
    loginAck = false;
    try {
      connection.sendBytes(buf.getBuffer());
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    if (waitForLogin) {
      if (waitForLogin()) {
        return true;
      } else if (isLoginRedirected()) {
        int port = redirectPort;
        disconnect();
        connect(port);
        return login();
      }
    }
    synchronized (lock) {
      return loginSuccess;
    }
  }
    /** {@inheritDoc} */
    public void bytesReceived(Connection conn, byte[] buffer) {
      if (connection != conn) {
        System.err.println(
            toString()
                + "AbstractDummyClient.Listener.bytesReceived:"
                + " wrong handle, got:"
                + conn
                + ", expected:"
                + connection);
        return;
      }

      MessageBuffer buf = new MessageBuffer(buffer);
      byte opcode = buf.getByte();

      handleOpCode(opcode, buf);
    }
    /**
     * {@inheritDoc}
     *
     * <p>Adds the specified {@code sessionId} to the per-channel cache for the given channel's
     * local member sessions, and sends a CHANNEL_JOIN protocol message to the session with the
     * corresponding {@code sessionId}.
     */
    public void join(String name, byte[] channelId, byte[] sessionId) {
      callStarted();
      try {
        if (logger.isLoggable(Level.FINEST)) {
          logger.log(
              Level.FINEST,
              "join channelId:{0} sessionId:{1}",
              HexDumper.toHexString(channelId),
              HexDumper.toHexString(sessionId));
        }

        // Update local channel membership cache.
        BigInteger channelRefId = new BigInteger(1, channelId);
        Set<BigInteger> localMembers = localChannelMembersMap.get(channelRefId);
        if (localMembers == null) {
          Set<BigInteger> newLocalMembers = Collections.synchronizedSet(new HashSet<BigInteger>());
          localMembers = localChannelMembersMap.putIfAbsent(channelRefId, newLocalMembers);
          if (localMembers == null) {
            localMembers = newLocalMembers;
          }
        }
        BigInteger sessionRefId = new BigInteger(1, sessionId);
        localMembers.add(sessionRefId);

        // Update per-session channel set cache.
        Set<BigInteger> channelSet = localPerSessionChannelsMap.get(sessionRefId);
        if (channelSet == null) {
          Set<BigInteger> newChannelSet = Collections.synchronizedSet(new HashSet<BigInteger>());
          channelSet = localPerSessionChannelsMap.putIfAbsent(sessionRefId, newChannelSet);
          if (channelSet == null) {
            channelSet = newChannelSet;
          }
        }
        channelSet.add(channelRefId);

        // Send CHANNEL_JOIN protocol message.
        MessageBuffer msg = new MessageBuffer(1 + MessageBuffer.getSize(name) + channelId.length);
        msg.putByte(SimpleSgsProtocol.CHANNEL_JOIN).putString(name).putBytes(channelId);
        sessionService.sendProtocolMessageNonTransactional(
            sessionRefId, ByteBuffer.wrap(msg.getBuffer()).asReadOnlyBuffer(), Delivery.RELIABLE);

      } finally {
        callFinished();
      }
    }
  /**
   * Gives a subclass a chance to handle an {@code opcode}. This implementation handles login,
   * redirection, relocation, and logout but does not handle session or channel messages.
   */
  protected void handleOpCode(byte opcode, MessageBuffer buf) {

    switch (opcode) {
      case SimpleSgsProtocol.LOGIN_SUCCESS:
        reconnectKey = buf.getBytes(buf.limit() - buf.position());
        newReconnectKey(reconnectKey);
        synchronized (lock) {
          loginAck = true;
          loginSuccess = true;
          System.err.println("login succeeded: " + name);
          lock.notifyAll();
        }
        sendMessage(new byte[0], true);
        break;

      case SimpleSgsProtocol.LOGIN_FAILURE:
        String failureReason = buf.getString();
        synchronized (lock) {
          loginAck = true;
          loginSuccess = false;
          System.err.println("login failed: " + name + ", reason:" + failureReason);
          lock.notifyAll();
        }
        break;

      case SimpleSgsProtocol.LOGIN_REDIRECT:
        redirectHost = buf.getString();
        redirectPort = buf.getInt();
        synchronized (lock) {
          loginAck = true;
          loginRedirect = true;
          System.err.println(
              "login redirected: " + name + ", host:" + redirectHost + ", port:" + redirectPort);
          lock.notifyAll();
        }
        break;

      case SimpleSgsProtocol.SUSPEND_MESSAGES:
        checkRelocateProtocolVersion();
        synchronized (lock) {
          if (suspendMessages) {
            break;
          }
          suspendMessages = true;
          if (waitForSuspendMessages) {
            waitForSuspendMessages = false;
            lock.notifyAll();
          } else {
            sendSuspendMessagesComplete();
          }
        }
        break;

      case SimpleSgsProtocol.RELOCATE_NOTIFICATION:
        checkRelocateProtocolVersion();
        relocateHost = buf.getString();
        relocatePort = buf.getInt();
        relocateKey = buf.getBytes(buf.limit() - buf.position());
        synchronized (lock) {
          relocateSession = true;
          System.err.println(
              "session to relocate: "
                  + name
                  + ", host:"
                  + relocateHost
                  + ", port:"
                  + relocatePort
                  + ", key:"
                  + HexDumper.toHexString(relocateKey));
          lock.notifyAll();
        }
        break;

      case SimpleSgsProtocol.RELOCATE_SUCCESS:
        checkRelocateProtocolVersion();
        reconnectKey = buf.getBytes(buf.limit() - buf.position());
        newReconnectKey(reconnectKey);
        synchronized (lock) {
          relocateAck = true;
          relocateSuccess = true;
          System.err.println("relocate succeeded: " + name);
          lock.notifyAll();
        }
        sendMessage(new byte[0], true);
        break;

      case SimpleSgsProtocol.RELOCATE_FAILURE:
        checkRelocateProtocolVersion();
        String relocateFailureReason = buf.getString();
        synchronized (lock) {
          relocateAck = true;
          relocateSuccess = false;
          System.err.println("relocate failed: " + name + ", reason:" + relocateFailureReason);
          lock.notifyAll();
        }
        break;

      case SimpleSgsProtocol.LOGOUT_SUCCESS:
        synchronized (lock) {
          logoutAck = true;
          System.err.println("logout succeeded: " + name);
          lock.notifyAll();
        }
        break;

      default:
        System.err.println(
            "WARNING: [" + name + "] dropping unknown op code: " + String.format("%02x", opcode));
        break;
    }
  }
    /**
     * {@inheritDoc}
     *
     * <p>Reads the local membership list for the specified {@code channelId}, and updates the local
     * membership cache for that channel. If any join or leave notifications were missed, then send
     * the appropriate CHANNEL_JOIN or CHANNEL_LEAVE protocol message to the effected session(s).
     */
    public void refresh(String name, byte[] channelId) {
      callStarted();
      if (logger.isLoggable(Level.FINE)) {
        logger.log(Level.FINE, "refreshing channelId:{0}", HexDumper.toHexString(channelId));
      }
      try {
        BigInteger channelRefId = new BigInteger(1, channelId);
        GetLocalMembersTask getMembersTask = new GetLocalMembersTask(channelRefId);
        try {
          transactionScheduler.runTask(getMembersTask, taskOwner);
        } catch (Exception e) {
          // FIXME: what is the right thing to do here?
          logger.logThrow(
              Level.WARNING,
              e,
              "obtaining members of channel:{0} throws",
              HexDumper.toHexString(channelId));
        }
        Set<BigInteger> newLocalMembers =
            Collections.synchronizedSet(getMembersTask.getLocalMembers());
        if (logger.isLoggable(Level.FINEST)) {
          logger.log(
              Level.FINEST, "newLocalMembers for channel:{0}", HexDumper.toHexString(channelId));
          for (BigInteger sessionRefId : newLocalMembers) {
            logger.log(
                Level.FINEST, "member:{0}", HexDumper.toHexString(sessionRefId.toByteArray()));
          }
        }

        /*
         * Determine which join and leave events were missed and
         * send protocol messages to clients accordingly.
         */
        Set<BigInteger> oldLocalMembers = localChannelMembersMap.put(channelRefId, newLocalMembers);
        Set<BigInteger> joiners = null;
        Set<BigInteger> leavers = null;
        if (oldLocalMembers == null) {
          joiners = newLocalMembers;
        } else {
          for (BigInteger sessionRefId : newLocalMembers) {
            if (oldLocalMembers.contains(sessionRefId)) {
              oldLocalMembers.remove(sessionRefId);
            } else {
              if (joiners == null) {
                joiners = new HashSet<BigInteger>();
              }
              joiners.add(sessionRefId);
            }
          }
          if (!oldLocalMembers.isEmpty()) {
            leavers = oldLocalMembers;
          }
        }
        if (joiners != null) {
          for (BigInteger sessionRefId : joiners) {
            MessageBuffer msg =
                new MessageBuffer(1 + MessageBuffer.getSize(name) + channelId.length);
            msg.putByte(SimpleSgsProtocol.CHANNEL_JOIN).putString(name).putBytes(channelId);
            sessionService.sendProtocolMessageNonTransactional(
                sessionRefId,
                ByteBuffer.wrap(msg.getBuffer()).asReadOnlyBuffer(),
                Delivery.RELIABLE);
          }
        }
        if (leavers != null) {
          for (BigInteger sessionRefId : leavers) {
            ByteBuffer msg = ByteBuffer.allocate(1 + channelId.length);
            msg.put(SimpleSgsProtocol.CHANNEL_LEAVE).put(channelId).flip();
            sessionService.sendProtocolMessageNonTransactional(
                sessionRefId, msg.asReadOnlyBuffer(), Delivery.RELIABLE);
          }
        }

      } finally {
        callFinished();
      }
    }