Package org.ch3ck3r.jgbx

Source Code of org.ch3ck3r.jgbx.JGBXConnector

package org.ch3ck3r.jgbx;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.helpers.NullEnumeration;
import org.ch3ck3r.jgbx.callbacks.GBXCallback;
import org.ch3ck3r.jgbx.error.JGBXException;
import org.ch3ck3r.jgbx.internal.JGBXFuture;
import org.ch3ck3r.jgbx.internal.RequestContainer;
import org.ch3ck3r.jgbx.requests.EnableCallbacksRequest;
import org.ch3ck3r.jgbx.requests.Request;
import org.ch3ck3r.jgbx.responses.Response;
import org.ch3ck3r.jgbx.utils.ByteOrderUtils;

/**
* The Java GBX Connector is a library to connect to your ManiaPlanet Server's GBX API (Trackmania2, Questmania,
* Shootmania). The GBX API of your ManiaPlanet Server enables you to communicate and control your server or to develop
* any kind of plugin for your game.
*
* @see <a href="https://bitbucket.org/Ch3ck3r/jgbx/overview">Visit us at BitBucket</a>
* @see <a href="https://bitbucket.org/Ch3ck3r/jgbx/wiki/Home">See the full documentation in our wiki</a>
* @author Frank Zechert
* @version 1
*/
public class JGBXConnector {
 
  /**
   * The logger
   */
  private static final Logger logger = Logger.getLogger(JGBXConnector.class);
 
  static {
    /*
     * Set up the basic configuration for log4j.
     */
    if (Logger.getRootLogger().getAllAppenders() instanceof NullEnumeration) {
      if (new File("etc/log4j.properties").exists()) {
        PropertyConfigurator.configure("etc/log4j.properties");
      }
      else if (new File("etc/log4j.xml").exists()) {
        PropertyConfigurator.configure("etc/log4j.xml");
      }
      else {
        BasicConfigurator.configure();
        logger.warn("Log4J is not configured on your system. A default configuration is used.");
        logger.warn("To get rid of this message configure Log4J by providing a log4j.properties file.");
      }
    }
  }
 
  /**
   * The length (in bytes) of the message length header.
   */
  private static final Short MESSAGE_LENGTH_HEADER_LENGTH = 4;
 
  /**
   * The length (in bytes) of the message handle header.
   */
  private static final Short MESSAGE_HANDLE_HEADER_LENGTH = 4;
 
  /**
   * The expected handshake answer from the server.
   */
  private static final String HANDSHAKE_ANSWER = "GBXRemote 2";
 
  /**
   * Size of the request queue.
   */
  private static final Integer REQUEST_QUEUE_SIZE = 100;
 
  /**
   * Size of the response queue.
   */
  private static final Integer RESPONSE_QUEUE_SIZE = 100;
 
  /**
   * The host to connect to.
   */
  private final InetAddress host;
 
  /**
   * The port to connect to
   */
  private final Integer port;
 
  /**
   * The connection timeout to use.
   */
  private final Integer connectionTimeout;
 
  /**
   * The timeout for receiving and sending data.
   */
  private Integer timeout;
 
  /**
   * The socket to connect to.
   */
  private final Socket socket;
 
  /**
   * Whether callbacks are enabled or not.
   */
  private boolean callbacksEnabled;
 
  /**
   * The input stream to receive data from the server.
   */
  private final InputStream inputStream;
 
  /**
   * The output stream to send data to the server.
   */
  private final OutputStream outputStream;
 
  /**
   * Whether there is an active connection or not.
   */
  private boolean isConnected;
 
  /**
   * Thread to send requests.
   */
  private final SenderThread senderThread;
 
  /**
   * Thread to receive responses and callbacks.
   */
  private final ReceiverThread receiverThread;
 
  /**
   * The next free message handle.
   */
  private Integer nextMessageHandle;
 
  /**
   * Lock to synchronize access to the message handle.
   */
  private final Object messageHandleLock;
 
  /**
   * The request queue.
   */
  private final ArrayBlockingQueue<RequestContainer> requestQueue;
 
  /**
   * The response queue.
   */
  private final ConcurrentHashMap<Integer, JGBXFuture<? extends Response>> responseQueue;
 
  /**
   * Connect to the given host or IP address. Use the default port 5000 and a default connection timeout of 5 seconds.
   *
   * @param host
   *            The host name or IP address to connect to.
   * @throws IOException
   *             The connection to the server failed.
   */
  public JGBXConnector(final String host) throws IOException {
    this(InetAddress.getByName(host));
  }
 
  /**
   * Connect to the given host or IP address on the given port. Use a default connection timeout of 5 seconds.
   *
   * @param host
   *            The host name or IP address to connect to.
   * @param port
   *            The port to connect to.
   * @throws IOException
   *             The connection to the server failed.
   */
  public JGBXConnector(final String host, final Integer port) throws IOException {
    this(InetAddress.getByName(host), port);
  }
 
  /**
   * Connect to the given host or IP address on the given port and a specified timeout value.
   *
   * @param host
   *            The host or IP address to connect to.
   * @param port
   *            The port to connect to.
   * @param connectionTimeout
   *            The connection timeout in milliseconds.
   * @throws IOException
   *             The connection to the server failed.
   */
  public JGBXConnector(final String host, final Integer port, final Integer connectionTimeout) throws IOException {
    this(InetAddress.getByName(host), port, connectionTimeout);
  }
 
  /**
   * Connect to the given host or IP address. Use the default port 5000 and a default connection timeout of 5 seconds.
   *
   * @param host
   *            The host name or IP address to connect to.
   * @throws IOException
   *             The connection to the server failed.
   */
  public JGBXConnector(final InetAddress host) throws IOException {
    this(host, 5000);
  }
 
  /**
   * Connect to the given host or IP address on the given port. Use a default connection timeout of 5 seconds.
   *
   * @param host
   *            The host name or IP address to connect to.
   * @param port
   *            The port to connect to.
   * @throws IOException
   *             The connection to the server failed.
   */
  public JGBXConnector(final InetAddress host, final Integer port) throws IOException {
    this(host, port, 5000);
  }
 
  /**
   * Connect to the given host or IP address on the given port and a specified timeout value.
   *
   * @param host
   *            The host or IP address to connect to.
   * @param port
   *            The port to connect to.
   * @param connectionTimeout
   *            The connection timeout in milliseconds.
   * @throws IOException
   *             The connection to the server failed.
   */
  public JGBXConnector(final InetAddress host, final Integer port, final Integer connectionTimeout)
    throws IOException {
    logger.debug("Creating a new JGBXConnector to " + host + ":" + port + " (" + connectionTimeout + "ms timeout)");
    this.callbacksEnabled = false;
    this.isConnected = false;
    this.timeout = 5000;
    this.nextMessageHandle = Integer.MIN_VALUE;
    this.messageHandleLock = new Object();
    this.requestQueue = new ArrayBlockingQueue<>(REQUEST_QUEUE_SIZE);
    this.responseQueue = new ConcurrentHashMap<>(RESPONSE_QUEUE_SIZE);
   
    this.host = host;
    this.port = port;
    this.connectionTimeout = connectionTimeout;
   
    try {
      this.socket = new Socket();
      this.socket.connect(new InetSocketAddress(host, port), connectionTimeout);
      logger.debug("The socket was connected successfully");
     
      this.inputStream = this.socket.getInputStream();
      this.outputStream = this.socket.getOutputStream();
     
      this.doHandshake();
    }
    catch (final IOException e) {
      logger.error("Failed to connect to the server.", e);
      throw e;
    }
   
    logger.info("Successfully connected to a ManiaPlanet server at " + host + ":" + port);
    this.isConnected = true;
    this.setTimeout(this.timeout);
   
    this.senderThread = new SenderThread(this, this.outputStream, this.requestQueue);
    this.receiverThread = new ReceiverThread(this, this.inputStream, this.responseQueue);
   
    this.senderThread.start();
    this.receiverThread.start();
  }
 
  /**
   * Sets the timeout after which the connection is considered lost when an incomplete message has been received.
   * The timeout is at 5 seconds by default.
   *
   * @param timeout
   *            The timeout in millisecond.
   */
  public void setTimeout(final Integer timeout) {
    if (this.socket == null) {
      final IllegalStateException ise = new IllegalStateException(
        "You can not call this method before the socket object is initialized.");
      logger.debug("Setting the SO timeout is not possible on a null socket", ise);
      throw ise;
    }
    this.timeout = timeout;
    try {
      this.socket.setSoTimeout(timeout);
      logger.debug("Socket SO timeout set to " + timeout + "ms");
    }
    catch (final SocketException e) {
      logger.error("Setting the socket SO timeout failed, disconnecting");
      this.disconnect();
    }
  }
 
  /**
   * Returns whether there is an active connection to the ManiaPlanet server.
   *
   * @return Returns true if the connection is established, false otherwise.
   */
  public boolean isConnected() {
    return this.isConnected;
  }
 
  /**
   * Disconnects from the ManiaPlanet server. If there is no established connection this method will do nothing.
   */
  public void disconnect() {
    if (!this.isConnected) {
      return;
    }
   
    this.senderThread.exit();
    this.receiverThread.exit();
    this.isConnected = false;
    this.requestQueue.clear();
   
    for (final JGBXFuture<?> futureResponse : this.responseQueue.values()) {
      futureResponse.fail(new JGBXException("Cancelled because JGBXConnector disconnected."));
    }
    this.responseQueue.clear();
   
    try {
      this.socket.close();
      logger.info("Disconnected from the ManiaPlanet Server at " + this.host + ":" + this.port);
    }
    catch (final IOException e) {
      // we can nothing do here but ignore the error
      logger.warn("Disconnecting from the ManiaPlanet Server failed", e);
    }
  }
 
  /**
   * Returns the IP address connected to or tried to connect to if connection failed.
   *
   * @return The IP address in textual representation.
   */
  public String getHost() {
    return this.host.getHostAddress();
  }
 
  /**
   * Return the InetAddress connected to or tried to connect to if connection failed.
   *
   * @return The InetAddress object representing the host.
   */
  public InetAddress getHostAddress() {
    return this.host;
  }
 
  /**
   * Return the port connected to or tried to connect to if connection failed.
   *
   * @return The port.
   */
  public Integer getPort() {
    return this.port;
  }
 
  /**
   * Returns the connection timeout used.
   *
   * @return The connection timeout in milliseconds.
   */
  public Integer getConnectionTimeout() {
    return this.connectionTimeout;
  }
 
  /**
   * Returns whether callbacks are enabled or not
   *
   * @return true if callbacks are enabled, false otherwise
   */
  public boolean isCallbacksEnabled() {
    return this.callbacksEnabled;
  }
 
  /**
   * Enable callbacks. This is equivalent to sending EnableCallbackRequest with true as a parameter.
   * If callbacks are already enabled this method does nothing.
   *
   * @throws JGBXException
   *             failed to enable callbacks.
   */
  public void enabledCallbacks() throws JGBXException {
    if (this.callbacksEnabled) {
      // callbacks are already enabled, do nothing
      return;
    }
    this.syncQuery(new EnableCallbacksRequest(true));
    this.callbacksEnabled = true;
  }
 
  /**
   * Disable callbacks. This is equivalent to sending EnableCallbackRequest with false as a parameter.
   * If callbacks are already disabled this method does nothing.
   *
   * @throws JGBXException
   *             failed to disable callbacks.
   */
  public void disableCallbacks() throws JGBXException {
    if (!this.callbacksEnabled) {
      // callbacks are already disabled, do nothing
      return;
    }
    this.syncQuery(new EnableCallbacksRequest(false));
    this.callbacksEnabled = false;
  }
 
  /**
   * Sends an asynchronous request to the server and returns a response of type T.
   * The response is returned immediately as a future object. This means that this
   * method call will not block. The future object will have a value as soon as the server sends a response.
   *
   * @param request
   *            The request to send.
   * @return The future object holding the response of type T.
   * @throws JGBXException
   *             Your request failed.
   */
  public <T extends Response> Future<T> asyncQuery(final Request request) throws JGBXException {
    final JGBXFuture<T> future = new JGBXFuture<T>(request.getResponseClass());
    this.addRequest(request, future);
    return future;
  }
 
  /**
   * Sends a synchronous request to the server and returns a response of type T.
   * The method call will block until the response is available.
   *
   * @param request
   *            The request to send.
   * @return The response sent by the server.
   * @throws JGBXException
   *             Your request failed.
   */
  public <T extends Response> T syncQuery(final Request request) throws JGBXException {
    final Future<T> response = this.<T> asyncQuery(request);
    try {
      return response.get();
    }
    catch (InterruptedException | ExecutionException e) {
      throw new JGBXException(e);
    }
  }
 
  /**
   * Add an event listener that is called when an event is received.
   *
   * @param listener
   *            The object to call.
   */
  public void addEventListener(final GBXCallback listener) {
   
  }
 
  /**
   * Remove an event listener.
   *
   * @param listener
   *            The event listener to remove
   */
  public void removeEventListener(final GBXCallback listener) {
   
  }
 
  /**
   * Perform a handshake with the connected server.
   * The handshake will validate that the connected server is
   * a ManiaPlanet server and not a server of a different kind.
   *
   * @throws IOException
   *             The handshake failed.
   */
  private void doHandshake() throws IOException {
    this.socket.setSoTimeout(this.connectionTimeout);
    final Integer length = this.getNextMessageLenght();
    if (length == -1) {
      throw new IOException(
        "Failed to do a handshake with the given server. No data was received from the connected peer.");
    }
    final String protocol = this.getNextMessage(length);
   
    logger.debug("Received handshake of length " + length + ": " + protocol);
    if (!protocol.equals(HANDSHAKE_ANSWER)) {
      logger.debug("The received handshake is invalid.");
      throw new IOException(
        "Connected server does not support the correct protocol. Ist his a ManiaPlanet server?");
    }
  }
 
  /**
   * Tries to read the next message length and returns it.
   * To get the next message length this method needs to read 4 bytes from the
   * socket stream. For reading the bytes the set SO timeout is used.
   * If reading the first byte fails (no data is available on the stream) no
   * message can be read from the stream. A message length of -1 is returned
   * by the function.
   * If reading the first byte succeeded but the timeout is hit for the following
   * bytes a connection problem is the cause. The method will then throw a
   * SocketTimeoutException or an IOException.
   *
   * @return the length of the next message
   * @throws SocketTimeoutException
   *             The length of the next message could not be read in time.
   * @throws IOException
   *             There was an error while reading from the socket.
   */
  Integer getNextMessageLenght() throws SocketTimeoutException, IOException {
    return this.getNextMessageLenght(this.timeout);
  }
 
  /**
   * Tries to read the next message length and returns it.
   * To get the next message length this method needs to read 4 bytes from the
   * socket stream. For reading the first byte the given timeout is used. For
   * all following bytes the set SO timeout is used.
   * If reading the first byte fails (no data is available on the stream) no
   * message can be read from the stream. A message length of -1 is returned
   * by the function.
   * If reading the first byte succeeded but the timeout is hit for the following
   * bytes a connection problem is the cause. The method will then throw a
   * SocketTimeoutException or an IOException.
   *
   * @param timeout
   *            timeout in milliseconds to wait for first byte
   * @return the length of the next message
   * @throws SocketTimeoutException
   *             The length of the next message could not be read in time.
   * @throws IOException
   *             There was an error while reading from the socket.
   */
  Integer getNextMessageLenght(final int timeout) throws SocketTimeoutException, IOException {
    final byte[] data = new byte[MESSAGE_LENGTH_HEADER_LENGTH];
    Integer read = 0;
    boolean timeoutReset = false;
    this.socket.setSoTimeout(timeout);
    try {
      do {
        read += this.inputStream.read(data, read, MESSAGE_LENGTH_HEADER_LENGTH - read);
        if (read > 0 && !timeoutReset) {
          this.socket.setSoTimeout(this.timeout);
          timeoutReset = true;
        }
      }
      while (read < MESSAGE_LENGTH_HEADER_LENGTH);
    }
    catch (final SocketTimeoutException e) {
      if (!timeoutReset) {
        this.socket.setSoTimeout(this.timeout);
      }
      if (read == 0) {
        return -1;
      }
      throw e;
    }
    if (!timeoutReset) {
      this.socket.setSoTimeout(this.timeout);
    }
    final Integer length = ByteOrderUtils.littleEndianessToInteger(data);
    if (length < 0) {
      throw new IOException("Received message was too long: " + length);
    }
    return length;
  }
 
  /**
   * Tries to read the next message handle and returns it.
   * If there is nothing to read, a TimeoutException is thrown after the SO timeout is reached.
   *
   * @return the handle of the next message
   * @throws SocketTimeoutException
   *             The handle of the next message could not be read in time.
   * @throws IOException
   *             There was an error while reading from the socket.
   */
  Integer getNextMessageHandle() throws SocketTimeoutException, IOException {
    final byte[] data = new byte[MESSAGE_HANDLE_HEADER_LENGTH];
    Integer read = 0;
    do {
      read += this.inputStream.read(data, read, MESSAGE_HANDLE_HEADER_LENGTH - read);
    }
    while (read < MESSAGE_HANDLE_HEADER_LENGTH);
    return ByteOrderUtils.littleEndianessToInteger(data);
  }
 
  /**
   * Reads the next message of the given length and returns it.
   * The message is decoded using the UTF-8 encoding. If the message can not
   * be read within the SO timeout an IOException is thrown.
   *
   * @param length
   *            The length of the message to read.
   * @return The XML-RPC message that was received.
   * @throws IOException
   *             Could not receive the message.
   */
  String getNextMessage(final Integer length) throws IOException {
    final byte[] data = new byte[length];
    Integer read = 0;
    do {
      read += this.inputStream.read(data, read, length - read);
    }
    while (read < length);
    return new String(data, "UTF-8");
  }
 
  /**
   * Add a request to the queue
   *
   * @param request
   *            The request to add.
   * @param future
   *            The future object for the response.
   * @throws JGBXException
   *             The request could not been queued.
   */
  <T extends Response> void addRequest(final Request request, final JGBXFuture<T> future) throws JGBXException {
    Integer handle;
    synchronized (this.messageHandleLock) {
      handle = this.nextMessageHandle;
      this.nextMessageHandle++;
      if (this.nextMessageHandle >= 0) {
        this.nextMessageHandle = Integer.MIN_VALUE;
      }
    }
   
    try {
      this.responseQueue.put(handle, future);
      this.requestQueue.put(new RequestContainer(handle, request));
      if (request instanceof EnableCallbacksRequest) {
        this.callbacksEnabled = request.<Boolean> getParameter(0);
      }
    }
    catch (final InterruptedException e) {
      this.responseQueue.remove(this.nextMessageHandle);
      throw new JGBXException(e);
    }
  }
}
TOP

Related Classes of org.ch3ck3r.jgbx.JGBXConnector

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.