Get up to 80 % extra points for free! More info:

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 in
  • LogoutEvent.EVENT_TYPE - response to the user logging out
  • ChatMessage.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.


 

Previous article
Java Chat - Client - Chat Service
All articles in this section
Server for Client Applications in Java
Skip article
(not recommended)
Java Chat - Client - Finishing Part 1
Article has been written for you by Petr Štechmüller
Avatar
User rating:
No one has rated this quite yet, be the first one!
Activities