package envoy.client.net; import java.io.Closeable; import java.io.IOException; import java.net.Socket; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import javax.naming.TimeLimitExceededException; import envoy.client.data.Cache; import envoy.client.data.Config; import envoy.client.data.LocalDb; import envoy.client.util.EnvoyLog; import envoy.data.*; import envoy.event.Event; import envoy.event.IdGeneratorRequest; import envoy.event.MessageStatusChangeEvent; import envoy.util.SerializationUtils; /** * Establishes a connection to the server, performs a handshake and delivers * certain objects to the server.
*
* Project: envoy-client
* File: Client.java
* Created: 28 Sep 2019
* * @author Kai S. K. Engelbart * @author Maximilian Käfer * @author Leon Hofmeister * @since Envoy v0.1-alpha */ public class Client implements Closeable { // Connection handling private Socket socket; private Receiver receiver; private boolean online; // Asynchronously initialized during handshake private volatile User sender; private volatile Contacts contacts; // Configuration and logging private static final Config config = Config.getInstance(); private static final Logger logger = EnvoyLog.getLogger(Client.class.getSimpleName()); /** * 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 localDb the local database used to persist the current * {@link IdGenerator} * @return a message cache containing all unread messages from the server that * can be relayed after initialization * @throws Exception if the online mode could not be entered or the request * failed for some other reason * @since Envoy v0.2-alpha */ public Cache onlineInit(LoginCredentials credentials, LocalDb localDb) throws Exception { // Establish TCP connection logger.info(String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); socket = new Socket(config.getServer(), config.getPort()); logger.info("Successfully connected to server."); // Create message receiver receiver = new Receiver(socket.getInputStream()); // Create cache for unread messages final Cache cache = new Cache<>(); // Register user creation processor, contact list processor and message cache receiver.registerProcessor(User.class, sender -> { logger.info("Acquired user object " + sender); this.sender = sender; }); receiver.registerProcessor(Contacts.class, contacts -> { logger.info("Acquired contacts object " + contacts); this.contacts = contacts; }); receiver.registerProcessor(Message.class, cache); // Start receiver new Thread(receiver).start(); // Write login credentials logger.finest("Sending login credentials..."); SerializationUtils.writeBytesWithLength(credentials, socket.getOutputStream()); // Wait for a maximum of five seconds to acquire the sender object long start = System.currentTimeMillis(); while (sender == null || contacts == null) { if (System.currentTimeMillis() - start > 5000) throw new TimeLimitExceededException("Did not log in after 5 seconds"); Thread.sleep(500); } logger.info("Handshake completed."); online = true; // Remove user creation processor receiver.removeAllProcessors(); // Register processors for message and status handling final ReceivedMessageProcessor receivedMessageProcessor = new ReceivedMessageProcessor(); receiver.registerProcessor(Message.class, receivedMessageProcessor); // Relay cached unread messages cache.setProcessor(receivedMessageProcessor); // Process message status changes receiver.registerProcessor(MessageStatusChangeEvent.class, new MessageStatusChangeEventProcessor()); // Process message ID generation receiver.registerProcessor(IdGenerator.class, localDb::setIdGenerator); // Request a generator if none is present or the existing one is consumed if (!localDb.hasIdGenerator() || !localDb.getIdGenerator().hasNext()) requestIdGenerator(); return cache; } /** * Creates a new write proxy that uses this client to communicate with the * server. * * @param localDb the local database that the write proxy will use to access * caches * @return a new write proxy * @since Envoy Client v0.3-alpha */ public WriteProxy createWriteProxy(LocalDb localDb) { return new WriteProxy(this, localDb); } /** * Sends a message to the server. The message's status will be incremented once * it was delivered successfully. * * @param message the message to send * @throws IOException if the message does not reach the server * @since Envoy v0.3-alpha */ public void sendMessage(Message message) throws IOException { writeObject(message); message.nextStatus(); } /** * Sends an event to the server. * * @param evt the event to send * @throws IOException if the event did not reach the server */ public void sendEvent(Event evt) throws IOException { writeObject(evt); } /** * Requests a new {@link IdGenerator} from the server. * * @throws IOException if the request does not reach the server * @since Envoy v0.3-alpha */ public void requestIdGenerator() throws IOException { logger.info("Requesting new id generator..."); writeObject(new IdGeneratorRequest()); } /** * @return a {@code Map} of all users on the server with their * user names as keys * @since Envoy v0.2-alpha */ public Map getUsers() { checkOnline(); Map users = new HashMap<>(); contacts.getContacts().forEach(u -> users.put(u.getName(), u)); return users; } @Override public void close() throws IOException { if (online) socket.close(); } private void writeObject(Object obj) throws IOException { checkOnline(); SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream()); } private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); } /** * @return the sender object that represents this client. * @since Envoy v0.1-alpha */ public User getSender() { return sender; } /** * Sets the client user which is used to send messages. * * @param sender the client user to set * @since Envoy v0.2-alpha */ public void setSender(User sender) { this.sender = sender; } /** * @return the {@link Receiver} used by this {@link Client} */ public Receiver getReceiver() { return receiver; } /** * @return {@code true} if a connection to the server could be established * @since Envoy v0.2-alpha */ public boolean isOnline() { return online; } }