All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.vrspace.client.VRSpaceClient Maven / Gradle / Ivy

package org.vrspace.client;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import org.vrspace.server.dto.ClientRequest;
import org.vrspace.server.dto.Command;
import org.vrspace.server.dto.Enter;
import org.vrspace.server.dto.Session;
import org.vrspace.server.dto.VREvent;
import org.vrspace.server.dto.Welcome;
import org.vrspace.server.obj.Client;

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.extern.slf4j.Slf4j;

/**
 * A simple vrspace websocket client.
 * 
 * @author joe
 *
 */
@Slf4j
public class VRSpaceClient implements WebSocket.Listener, Runnable {
  private ObjectMapper mapper;
  private URI uri;
  private WebSocket ws;
  private List> messageListeners = new ArrayList<>();
  private List> welcomeListeners = new ArrayList<>();
  private List> eventListeners = new ArrayList<>();
  private StringBuilder text = new StringBuilder();
  private CountDownLatch latch;
  private volatile Client client;
  private int errorCount = 0;
  private ScheduledFuture task;
  private String world = null;
  private CompletableFuture future;
  private Map settings = null;
  public static final long TIMEOUT = 5000;
  public static final long RETRY = 10000;

  public VRSpaceClient(URI uri, ObjectMapper mapper) {
    this.uri = uri;
    this.mapper = mapper;
  }

  public CompletableFuture connect() {
    latch = new CountDownLatch(1);
    future = new CompletableFuture<>();
    this.task = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(this, 0, RETRY, TimeUnit.MILLISECONDS);
    return future;
  }

  public void startSession() {
    send(new Session());
  }

  public void connectAndEnter(String world) {
    connectAndEnter(world, settings);
  }

  public void connectAndEnter(String world, Map params) {
    connect().thenApply(ws -> {
      await();
      if (params != null) {
        this.settings = params;
        ClientRequest settings = new ClientRequest(getClient());
        params.entrySet().forEach((e) -> settings.addChange(e.getKey(), e.getValue()));
        send(settings);
      }
      enter(world);
      await();
      send(new Session());
      return ws;
    });
  }

  @Override
  public void run() {
    HttpClient.newHttpClient().newWebSocketBuilder().connectTimeout(Duration.ofMillis(TIMEOUT)).buildAsync(uri, this)
        .thenApply(ws -> {
          this.ws = ws;
          this.task.cancel(true);
          this.future.complete(ws);
          return ws;
        }).handle((ws, exception) -> {
          if (exception != null) {
            // too verbose
            // log.error("Websocket exception connecting to " + uri, exception);
            log.error("Websocket exception connecting to " + uri + " - " + exception);
          }
          return null;
        }).join();
  }

  /**
   * Add event listener to receive events from the server; an event is either a
   * Command or change to a VRObject
   */
  public VRSpaceClient addEventListener(Function listener) {
    this.eventListeners.add(listener);
    return this;
  }

  /** Add a listener that receives all text messages from the server */
  public VRSpaceClient addMessageListener(Function listener) {
    this.messageListeners.add(listener);
    return this;
  }

  /** Welcome messages are received after connecting and entering a world */
  public VRSpaceClient addWelcomeListener(Function listener) {
    this.welcomeListeners.add(listener);
    return this;
  }

  /** Hack, awaits for welcome message */
  public void await() {
    try {
      latch.await();
    } catch (InterruptedException e) {
      log.error("Unexpected interrupt: ", e);
    }
  }

  public Client getClient() {
    return this.client;
  }

  /** Enter a world */
  public void enter(String world) {
    send(new Enter(world));
    this.world = world;
  }

  /** Send a json string to the server */
  public void send(String arg) {
    this.ws.sendText(arg, true);
  }

  /** Send a request */
  public void send(ClientRequest req) {
    try {
      String text = mapper.writeValueAsString(req);
      log.debug("Sending " + text);
      this.ws.sendText(text, true);
    } catch (Exception e) {
      log.error("OOPS", e);
    }
  }

  /** Send a command */
  public void send(Command cmd) {
    ClientRequest req = new ClientRequest(this.client, cmd);
    send(req);
  }

  public int getErrorCount() {
    return errorCount;
  }

  @Override
  public void onOpen(WebSocket webSocket) {
    log.info("Connected to " + uri);
    WebSocket.Listener.super.onOpen(webSocket);
  }

  @Override
  public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) {
    log.debug("Received " + last + ":" + data);
    text.append(data);
    if (last) {
      String message = text.toString();
      messageListeners.forEach(l -> l.apply(message));
      text = new StringBuilder();
      try {
        // TODO this should work with deserialization out of the box
        // introduce ERROR class etc
        if (message.startsWith("{\"Welcome\":{")) {
          Welcome welcome = mapper.readValue(message, Welcome.class);
          this.client = welcome.getClient();
          latch.countDown();
          welcomeListeners.forEach(l -> l.apply(welcome));
        } else if (message.startsWith("{\"ERROR\"")) {
          errorCount++;
        } else {
          VREvent event = mapper.readValue(message, VREvent.class);
          eventListeners.forEach(l -> l.apply(event));
        }
      } catch (Exception e) {
        log.error("Message parsing or processing error", e);
      }
    }

    CompletionStage ret = WebSocket.Listener.super.onText(webSocket, data, last);
    return ret;
  }

  @Override
  public void onError(WebSocket webSocket, Throwable error) {
    log.error("Websocket error " + webSocket);
    connectAndEnter(this.world);
    WebSocket.Listener.super.onError(webSocket, error);
  }

  @Override
  public CompletionStage onPing(WebSocket webSocket, ByteBuffer message) {
    // log.debug("Ping received " + webSocket);
    return WebSocket.Listener.super.onPing(webSocket, message);
  }

  @Override
  public CompletionStage onClose(WebSocket webSocket, int statusCode, String reason) {
    log.debug("Socket closed: " + statusCode + " " + reason);
    connectAndEnter(this.world);
    return WebSocket.Listener.super.onClose(webSocket, statusCode, reason);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy