package chatty.util.api.pubsub;
import chatty.util.DateTime;
import static chatty.util.MiscUtil.getStackTrace;
import chatty.util.TimedCounter;
import chatty.util.api.pubsub.Client.MyConfigurator;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.websocket.ClientEndpoint;
import javax.websocket.ClientEndpointConfig;
import javax.websocket.CloseReason;
import javax.websocket.HandshakeResponse;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import org.glassfish.tyrus.client.ClientManager;
import org.glassfish.tyrus.client.ClientProperties;
/**
*
* @author tduva
*/
@ClientEndpoint(configurator = MyConfigurator.class)
public class Client {
private static final Logger LOGGER = Logger.getLogger(Client.class.getName());
private final MessageHandler handler;
private final TimedCounter disconnectsPerHour = new TimedCounter(60*60*1000, 0);
private int connectionAttempts;
private boolean connecting;
private boolean requestedDisconnect;
private Session s;
private long connectedSince;
private long lastMessageReceived;
public Client(MessageHandler handler) {
this.handler = handler;
}
public synchronized long getLastMessageReceived() {
return lastMessageReceived;
}
public synchronized String getStatus() {
if (!connecting) {
return "Not connected";
}
if (s != null && s.isOpen()) {
return String.format("Connected to %s for %s (last message received %s ago)",
s.getRequestURI(),
DateTime.ago(connectedSince),
DateTime.ago(lastMessageReceived));
}
return "Connecting..";
}
public static class MyConfigurator extends ClientEndpointConfig.Configurator {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
// Empty Origin, otherwise would default to server name
headers.put("Origin", Arrays.asList(""));
}
@Override
public void afterResponse(HandshakeResponse hr) {
}
}
private class Reconnect extends ClientManager.ReconnectHandler {
@Override
public boolean onDisconnect(CloseReason closeReason) {
if (requestedDisconnect) {
return false;
}
disconnectsPerHour.increase();
connectionAttempts++;
LOGGER.info("[PubSub] Reconnecting in "+getDelay()+"s");
return true;
}
@Override
public boolean onConnectFailure(Exception exception) {
if (requestedDisconnect) {
LOGGER.info("[PubSub] Cancelled reconnecting..");
return false;
}
connectionAttempts++;
LOGGER.info(String.format("[PubSub] Another connection attempt (%d) in %ds [%s/%s]",
connectionAttempts,
getDelay(),
exception,
exception.getCause().toString()));
return true;
}
@Override
public long getDelay() {
/**
* Wait longer if connection doesn't succeed, however too many
* disconnects after a successful connections in a short period of
* time should slow down connecting as well, just in case.
*/
int disconnects = disconnectsPerHour.getCount();
return connectionAttempts*connectionAttempts+disconnects*disconnects;
}
}
public synchronized void connect(String server) {
/**
* Only connect once, which is intended to stay connected forever. If
* manually disconnecting/connecting again should be a thing, some stuff
* may have to be changed.
*/
if (connecting) {
return;
}
connecting = true;
try {
LOGGER.info("[PubSub] Connecting to "+server);
ClientManager clientManager = ClientManager.createClient();
clientManager.getProperties().put(ClientProperties.RECONNECT_HANDLER, new Reconnect());
clientManager.asyncConnectToServer(this, new URI(server));
} catch (Exception ex) {
LOGGER.warning("[PubSub] Error connecting "+ex);
}
}
public synchronized void disconnect() {
requestedDisconnect = true;
close();
}
public synchronized void reconnect() {
close();
}
/**
* Close connection. If requestedDisconnect is false, it will automatically
* reconnect.
*/
private synchronized void close() {
if (s != null) {
try {
s.close();
} catch (IOException ex) {
LOGGER.warning("[PubSub] Error disconnecting "+ex);
}
}
}
public synchronized void send(String message) {
if (s != null && s.isOpen()) {
s.getAsyncRemote().sendText(message);
handler.handleSent(message);
}
}
@OnOpen
public synchronized void onOpen(Session session) {
LOGGER.info("[PubSub] Connected: "+session.getRequestURI());
connectedSince = System.currentTimeMillis();
this.s = session;
handler.handleConnect();
}
@OnMessage
public synchronized void onMessage(String message, Session session) {
lastMessageReceived = System.currentTimeMillis();
handler.handleReceived(message);
}
@OnClose
public synchronized void onClose(Session session, CloseReason closeReason) {
LOGGER.info(String.format("[PubSub] Connection closed after %s [%s]",
DateTime.ago(connectedSince),
closeReason));
s = null;
handler.handleDisconnect();
}
@OnError
public void onError(Session session, Throwable t) {
LOGGER.warning("[PubSub] ERROR: "+getStackTrace(t));
}
public interface MessageHandler {
public void handleReceived(String text);
public void handleSent(String text);
public void handleConnect();
public void handleDisconnect();
}
}