package envoy.client.net; import java.io.*; import java.net.Socket; import java.util.concurrent.TimeoutException; import java.util.logging.*; import envoy.client.data.*; import envoy.client.event.EnvoyCloseEvent; import envoy.data.*; import envoy.event.*; import envoy.util.*; import dev.kske.eventbus.*; import dev.kske.eventbus.Event; /** * Establishes a connection to the server, performs a handshake and delivers * certain objects to the server. * * @author Kai S. K. Engelbart * @author Maximilian Käfer * @author Leon Hofmeister * @since Envoy Client v0.1-alpha */ public final class Client implements EventListener, Closeable { // Connection handling private Socket socket; private Receiver receiver; private boolean online; // Asynchronously initialized during handshake private volatile User sender; private volatile boolean rejected; // Configuration, logging and event management private static final ClientConfig config = ClientConfig.getInstance(); private static final Logger logger = EnvoyLog.getLogger(Client.class); private static final EventBus eventBus = EventBus.getInstance(); /** * Constructs a client and registers it as an event listener. * * @since Envoy Client v0.2-beta */ public Client() { eventBus.registerListener(this); } /** * Enters the online mode by acquiring a user ID from the server. As a * connection has to be established and a handshake has to be made, this method * will block for up to 5 seconds. If the handshake does exceed this time limit, * an exception is thrown. * * @param credentials the login credentials of the user * @param cacheMap the map of all caches needed * @throws TimeoutException if the server could not be reached * @throws IOException if the login credentials could not be written * @throws InterruptedException if the current thread is interrupted while * waiting for the handshake response */ public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException { if (online) throw new IllegalStateException("Handshake has already been performed successfully"); rejected = false; // Establish TCP connection logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); socket = new Socket(config.getServer(), config.getPort()); logger.log(Level.FINE, "Successfully established TCP connection to server"); // Create object receiver receiver = new Receiver(socket.getInputStream()); // Register user creation processor, contact list processor, message cache and // authentication token receiver.registerProcessor(User.class, sender -> this.sender = sender); receiver.registerProcessors(cacheMap.getMap()); // Start receiver receiver.start(); // Write login credentials SerializationUtils.writeBytesWithLength(credentials, socket.getOutputStream()); // Wait for a maximum of five seconds to acquire the sender object final long start = System.currentTimeMillis(); while (sender == null) { // Quit immediately after handshake rejection // This method can then be called again if (rejected) { socket.close(); receiver.removeAllProcessors(); return; } if (System.currentTimeMillis() - start > 5000) { rejected = true; throw new TimeoutException("Did not log in after 5 seconds"); } Thread.sleep(500); } online = true; logger.log(Level.INFO, "Handshake completed."); } /** * Initializes the {@link Receiver} used to process data sent from the server to * this client. * * @param localDB the local database used to persist the current * {@link IDGenerator} * @param cacheMap the map of all caches needed * @throws IOException if no {@link IDGenerator} is present and none could be * requested from the server * @since Envoy Client v0.2-alpha */ public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException { checkOnline(); // Remove all processors as they are only used during the handshake receiver.removeAllProcessors(); // Relay cached messages and message status changes cacheMap.get(Message.class).setProcessor(eventBus::dispatch); cacheMap.get(GroupMessage.class).setProcessor(eventBus::dispatch); cacheMap.get(MessageStatusChange.class).setProcessor(eventBus::dispatch); cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch); // Request a generator if none is present or the existing one is consumed if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIDGenerator(); // Relay caches cacheMap.getMap().values().forEach(Cache::relay); } /** * Sends an object to the server. * * @param obj the object to send * @throws IllegalStateException if the client is not online * @throws RuntimeException if the object serialization failed * @since Envoy Client v0.2-beta */ public void send(Serializable obj) throws IllegalStateException, RuntimeException { checkOnline(); logger.log(Level.FINE, "Sending " + obj); try { SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream()); } catch (final IOException e) { throw new RuntimeException(e); } } /** * Sends a message to the server. The message's status will be incremented once * it was delivered successfully. * * @param message the message to send * @since Envoy Client v0.3-alpha */ public void sendMessage(Message message) { send(message); message.nextStatus(); } /** * Requests a new {@link IDGenerator} from the server. * * @since Envoy Client v0.3-alpha */ public void requestIDGenerator() { logger.log(Level.INFO, "Requesting new id generator..."); send(new IDGeneratorRequest()); } @Event(eventType = HandshakeRejection.class, priority = 1000) private void onHandshakeRejection() { rejected = true; } @Override @Event(eventType = EnvoyCloseEvent.class, priority = 50) public void close() { if (online) { logger.log(Level.INFO, "Closing connection..."); try { // The sender must be reset as otherwise the handshake is immediately closed sender = null; online = false; socket.close(); } catch (final IOException e) { logger.log(Level.WARNING, "Failed to close socket: ", e); } } } /** * Ensured that the client is online. * * @throws IllegalStateException if the client is not online * @since Envoy Client v0.3-alpha */ private void checkOnline() throws IllegalStateException { if (!online) throw new IllegalStateException("Client is not online"); } /** * @return the {@link User} as which this client is logged in * @since Envoy Client v0.1-alpha */ public User getSender() { return sender; } /** * Sets the client user which is used to send messages. * * @param clientUser the client user to set * @since Envoy Client v0.2-alpha */ public void setSender(User clientUser) { sender = clientUser; } /** * @return the {@link Receiver} used by this {@link Client} * @since v0.2-alpha */ public Receiver getReceiver() { return receiver; } /** * @return {@code true} if a connection to the server could be established * @since Envoy Client v0.2-alpha */ public boolean isOnline() { return online; } }