Lesson 22 - Java Chat - Server - Chat Plugin
In the previous lesson, Java Chat - Client - Chat Service, we created a chat base.
In today's Java tutorial, we're going to implement a chat plugin for the server.
ChatPlugin
We'll create a new server plugin that will be in charge of the chat
functions. Let's start by creating a new chat
package in the
plugins
package. We'll create a ChatPlugin
class:
package cz.stechy.chat.plugins.chat; @Singleton public class ChatPlugin implements IPlugin { public static final String PLUGIN_NAME = "chat"; private void loginEventHandler(IEvent event) {} private void logoutEventHandler(IEvent event) {} private void chatMessageHandler(IEvent event) {} @Override public String getName() { return PLUGIN_NAME; } @Override public void init() { System.out.println("Initializing chat plugin."); } @Override public void registerMessageHandlers(IEventBus eventBus) { eventBus.registerEventHandler(LoginEvent.EVENT_TYPE, this::loginEventHandler); eventBus.registerEventHandler(LogoutEvent.EVENT_TYPE, this::logoutEventHandler); eventBus.registerEventHandler(ChatMessage.MESSAGE_TYPE, this::chatMessageHandler); } }
The class implements the standard IPlugin
interface. In the
registerMessageHandlers()
method, we register three handlers:
LoginEvent.EVENT_TYPE
- response to the user logging inLogoutEvent.EVENT_TYPE
- response to the user logging outChatMessage.MESSAGE_TYPE
- response to the chat message itself
We'll implement the handler bodies when implementing the service. We'll
register the plug-in directly in the Plugin
enumeration:
CHAT(ChatPlugin.class);
ChatService
All the logic will be in the ChatService
class. So we'll create
a new service
package including an IChatService
interface and a ChatService
class implementing this interface:
package cz.stechy.chat.plugins.chat.service; @ImplementedBy(ChatService.class) public interface IChatService { void addClient(IClient client, String id, String name); void removeClient(String id); void sendMessage(String destinationClientId, String sourceClientId, byte[] rawMessage); Optional <String> findIdByClient(IClient client); void informClientIsTyping(String destinationClientId, String sourceClientId, boolean typing); }
The addClient()
/ removeClient()
methods will
manage the chat client entries. You may ask why doing this in a complicated way
if we could use AuthService
, which already contains the client
entries. It's important to note that the user connection and login processes are
independent of each other. Therefore, a connected user may not want to log in.
The sendMessage()
method accepts the
destinationClientId
and sourceClientId
. These
parameters represent the id of the destination and source client so that the
server knows who the message is from and to whom the message belongs. The
informClientIsTyping()
method will inform the target client that
the client has started / stopped typing.
Implementing the Interface
Now we'll implement all the methods, one by one. The
ChatService()
class implements the IChatService
interface:
@Singleton class ChatService implements IChatService { }
In this class we'll create a private static internal ChatClient
class, which will serve only as a container. The class will contain the
IClient
instance and client's name:
private static final class ChatClient { final IClient client; final String name; private ChatClient(IClient client, String name) { this.client = client; this.name = name; } }
In the ChatService
class, we'll create a class constant that
will contain a map of all clients in the chat:
private final Map<String, ChatClient> clients = new HashMap<>();
Next, we'll create a private helper method to send messages to all connected clients:
private void broadcastMessage(IMessage message) { clients.values().forEach(chatClient -> chatClient.client.sendMessageAsync(message)); }
We'll start with the addClient()
method:
@Override public synchronized void addClient(IClient client, String id, String name) { final ChatClient chatClient = new ChatClient(client, name); clients.forEach((clientId, entry) -> client.sendMessageAsync(new ChatMessage( new ChatMessageAdministrationData( new ChatMessageAdministrationClientState( ChatAction.CLIENT_CONNECTED, clientId, entry.name))))); clients.put(id, chatClient); broadcastMessage(new ChatMessage( new ChatMessageAdministrationData( new ChatMessageAdministrationClientState( ChatAction.CLIENT_CONNECTED, id, name)))); }
When adding a new client to the map, we'll first send a list of all connected clients to that client. Only then we'll add it to our collection. Finally, we'll send all connected clients (even the new one) that a new client has joined. This trick will allow each user to write with himself.
The method for removing a client, removeClient()
, will be as
follows:
@Override public synchronized void removeClient(String id) { clients.remove(id); broadcastMessage(new ChatMessage( new ChatMessageAdministrationData( new ChatMessageAdministrationClientState( ChatAction.CLIENT_DISCONNECTED, id)))); }
First, we remove the client from the map. Then we send a message to the other clients that someone has logged out.
The method for sending a message will be very simple:
@Override public void sendMessage(String destinationClientId, String sourceClientId, byte[] rawMessage) { clients.get(destinationClientId).client.sendMessageAsync(new ChatMessage(new ChatMessageCommunicationData(sourceClientId, rawMessage))); }
It gets the target client by its client ID from the map and sends the message from a parameter to it.
The last but one method to implement is
informClientIsTyping()
:
@Override public void informClientIsTyping(String destinationClientId, String sourceClientId, boolean typing) { clients.get(destinationClientId).client.sendMessageAsync( new ChatMessage( new ChatMessageAdministrationData( new ChatMessageAdministrationClientTyping( typing ? ChatAction.CLIENT_TYPING : ChatAction.CLIENT_NOT_TYPING, sourceClientId )))); }
Again, we get the target client from the client map and send a message to it, informing whether the client is writing or has stopped writing.
The last method we're going to implement is the findIdByClient()
method. Using this method, we'll look for the client ID based on the
IClient
instance:
@Override public Optional <String> findIdByClient(IClient client) { final Optional <Entry <String, ChatClient>> entryOptional = clients.entrySet() .stream() .filter(entry -> entry.getValue().client == client) .findFirst(); return entryOptional.map(Entry::getKey); }
In the filter, we compare using ==
. We can afford this because
we're sure that such an instance is there.
Finishing the Plugin
We'll now return to the ChatPlugin
class to complete the method
bodies. Before we do this, we'll create a new IChatService
instance
constant and initialize it in the constructor:
private final IChatService chatService; @Inject public ChatPlugin(IChatService chatService) { this.chatService = chatService; }
The loginEventHandler()
and logoutEventHandler()
method bodies will add / remove clients from chatService
:
private void loginEventHandler(IEvent event) { final LoginEvent loginEvent = (LoginEvent) event; chatService.addClient(loginEvent.client, loginEvent.user.id, loginEvent.user.name); } private void logoutEventHandler(IEvent event) { final LogoutEvent logoutEvent = (LogoutEvent) event; chatService.removeClient(logoutEvent.user.id); }
We'll implement the chatMessageHandler()
method step by step
since it's longer. First we'll get data from the message:
final MessageReceivedEvent messageReceivedEvent = (MessageReceivedEvent) event; final IClient client = messageReceivedEvent.getClient(); final ChatMessage chatMessage = (ChatMessage) messageReceivedEvent.getReceivedMessage(); final IChatMessageData chatMessageData = (IChatMessageData) chatMessage.getData();
This is followed by a very similar branching as on the client's side. We get
the message type again using the getDataType()
method. If the
message is administrative, we'll get the necessary data from it and decide what
action to take. If it'll be sending a message, we'll send the message to the
correct client. The branching looks as follows:
switch (chatMessageData.getDataType()) { case DATA_ADMINISTRATION: IChatMessageAdministrationData administrationData = (IChatMessageAdministrationData) chatMessageData.getData(); switch (administrationData.getAction()) { case CLIENT_REQUEST_CONNECT: final ChatMessageAdministrationClientRequestConnect clientRequestConnect = (ChatMessageAdministrationClientRequestConnect) administrationData; final String clientId = clientRequestConnect.getId(); final String clientName = clientRequestConnect.getName(); chatService.addClient(client, clientId, clientName); break; case CLIENT_DISCONNECTED: final ChatMessageAdministrationClientState clientDisconnected = (ChatMessageAdministrationClientState) administrationData; final String disconnectedClientId = clientDisconnected.getId(); chatService.removeClient(disconnectedClientId); break; case CLIENT_TYPING: final ChatMessageAdministrationClientTyping clientIsTyping = (ChatMessageAdministrationClientTyping) administrationData; final String typingClientId = clientIsTyping.getId(); chatService.informClientIsTyping(typingClientId, chatService.findIdByClient(client).orElse(""), true); break; case CLIENT_NOT_TYPING: final ChatMessageAdministrationClientTyping clientIsNotTyping = (ChatMessageAdministrationClientTyping) administrationData; final String notTypingClientId = clientIsNotTyping.getId(); chatService.informClientIsTyping(notTypingClientId, chatService.findIdByClient(client).orElse(""), false); break; default: throw new IllegalArgumentException("Invalid argument. " + administrationData.getAction()); } break; case DATA_COMMUNICATION: final ChatMessageCommunicationDataContent communicationDataContent = (ChatMessageCommunicationDataContent) chatMessageData.getData(); final String destinationClientId = communicationDataContent.getDestination(); final String sourceClientId = chatService.findIdByClient(client).orElse(""); final byte[] rawMessage = communicationDataContent.getData(); chatService.sendMessage(destinationClientId, sourceClientId, rawMessage); break; default: throw new IllegalArgumentException("Invalid argument." + chatMessageData.getDataType()); }
By doing so, we should have a complete server functionality.
In the next lesson, Java Chat - Client - Finishing Part 1, we'll start implementing the features to the GUI client.