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

Lesson 21 - Java Chat - Client - Chat Service

In the previous lesson, Java Chat - Server - User Management, we created user management on the server side.

In today's Java tutorial, we're going to start building the basics of the chat. First we'll create a class to send chat messages. We'll also design and implement an interface for the chat service, which will be used to access the chat functions.

Chat Message

In the share module, in the message package, we'll create a new ChatMessage class to send all chat-related messages. The class is large, we'll show its source code first and then visualize it on a diagram:

package cz.stechy.chat.net.message;

public class ChatMessage implements IMessage {

    private static final long serialVersionUID = -7817515518938131863L;

    public static final String MESSAGE_TYPE = "chat";

    private final IChatMessageData data;

    public ChatMessage(IChatMessageData data) {
        this.data = data;
    }

    @Override
    public String getType() {
        return MESSAGE_TYPE;
    }

    @Override
    public Object getData() {
        return data;
    }

    public interface IChatMessageData extends Serializable {

        ChatMessageDataType getDataType();

        Object getData();

    }

    public enum ChatMessageDataType {
        DATA_ADMINISTRATION,
        DATA_COMMUNICATION
    }

    public static final class ChatMessageAdministrationData implements IChatMessageData {

        private static final long serialVersionUID = 8237826895694688852L;

        private final IChatMessageAdministrationData data;

        public ChatMessageAdministrationData(IChatMessageAdministrationData data) {
            this.data = data;
        }

        @Override
        public ChatMessageDataType getDataType() {
            return ChatMessageDataType.DATA_ADMINISTRATION;
        }

        @Override
        public Object getData() {
            return data;
        }

        public enum ChatAction {
            CLIENT_REQUEST_CONNECT, // chat service connection request
            CLIENT_CONNECTED,
            CLIENT_DISCONNECTED, // clients' actions
            CLIENT_TYPING,
            CLIENT_NOT_TYPING, // information that somebody is typing
        }

        public interface IChatMessageAdministrationData extends Serializable {
            ChatAction getAction();
        }

        public static final class ChatMessageAdministrationClientRequestConnect implements IChatMessageAdministrationData {

            private static final long serialVersionUID = 642524654412490721L;

            private final String id;
            private final String name;

            public ChatMessageAdministrationClientRequestConnect(String id, String name) {
                this.id = id;
                this.name = name;
            }

            public String getId() {
                return id;
            }

            public String getName() {
                return name;
            }

            @Override
            public ChatAction getAction() {
                return ChatAction.CLIENT_REQUEST_CONNECT;
            }
        }

        public static final class ChatMessageAdministrationClientState implements IChatMessageAdministrationData {

            private static final long serialVersionUID = -6101992378764622660L;

            private final ChatAction action;
            private final String id;
            private final String name;

            public ChatMessageAdministrationClientState(ChatAction action, String id) {
                this(action, id, "");
            }

            public ChatMessageAdministrationClientState(ChatAction action, String id, String name) {
                this.id = id;
                this.name = name;
                assert action == ChatAction.CLIENT_CONNECTED || action == ChatAction.CLIENT_DISCONNECTED;
                this.action = action;
            }

            public String getId() {
                return id;
            }

            public String getName() {
                return name;
            }

            @Override
            public ChatAction getAction() {
                return action;
            }
        }

        public static final class ChatMessageAdministrationClientTyping implements IChatMessageAdministrationData {

            private static final long serialVersionUID = 630432882631419944L;

            private final ChatAction action;
            private final String id;

            public ChatMessageAdministrationClientTyping(ChatAction action, String id) {
                assert action == ChatAction.CLIENT_TYPING || action == ChatAction.CLIENT_NOT_TYPING;
                this.action = action;
                this.id = id;
            }

            public String getId() {
                return id;
            }

            @Override
            public ChatAction getAction() {
                return action;
            }
        }
    }

    public static final class ChatMessageCommunicationData implements IChatMessageData {

        private static final long serialVersionUID = -2426630119019364058L;

        private final ChatMessageCommunicationDataContent data;

        public ChatMessageCommunicationData(String id, byte[] data) {
            this.data = new ChatMessageCommunicationDataContent(id, data);
        }

        @Override
        public ChatMessageDataType getDataType() {
            return ChatMessageDataType.DATA_COMMUNICATION;
        }

        @Override
        public Object getData() {
            return data;
        }

        public static final class ChatMessageCommunicationDataContent implements Serializable {

            private static final long serialVersionUID = -905319575968060192L;

            private final String destination;
            private final byte[] data;

            ChatMessageCommunicationDataContent(String destination, byte[] data) {
                this.destination = destination;
                this.data = data;
            }

            public String getDestination() {
                return destination;
            }

            public byte[] getData() {
                return data;
            }
        }
    }
}

We'll visualize the class due to its size. See the picture below:

Class diagram to represent the chat message - Server for Client Applications in Java

ChatContact

Now we'll create a class to represent one contact of the client. We'll name the class ChatContact and place it in the model package:

public class ChatContact {

    private final ObservableList <ChatMessageEntry> messages = FXCollections.observableArrayList();
    private final StringProperty name = new SimpleStringProperty(this, "name", null);
    private final ObjectProperty <Color> contactColor = new SimpleObjectProperty<>(this, "contactColor", null);
    private final IntegerProperty unreadedMessages = new SimpleIntegerProperty(this, "unreadedMessages", 0);
    private final BooleanProperty typing = new SimpleBooleanProperty(this, "typing", false);
    private final String id;

    public ChatContact(String id, String name) {
        this.id = id;
        this.name.set(name);
        contactColor.set(Color.color(Math.random(), Math.random(), Math.random()));
    }

    public void addMessage(ChatContact chatContact, String message) {
        messages.add(new ChatMessageEntry(chatContact, message));
        unreadedMessages.set(unreadedMessages.get() + 1);
    }

    public void resetUnreadedMessages() {
        unreadedMessages.set(0);
    }

    public void setTyping() {
        typing.set(true);
    }

    public void resetTyping() {
        typing.set(false);
    }

    public ObservableList <ChatMessageEntry> getMessages() {
        return messages;
    }

    @Override
    public String toString() {
        return getName();
    }
}

The messages instance constant contains a collection of messages that the user exchanged with that contact. The meaning of the other constants should be self-explanatory. The typing BooleanProperty indicates whether the contact is currently typing a message or not. The addMessage() method adds a new message to the messages collection. We'll use the setTyping() and resetTyping() methods to set whether the contact is typing or not. I'm sure we don't have to describe the getters and setters here.

We've already used a class that we haven't created yet, ChatMessageEntry, so let's add it.

ChatMessageEntry

This class will represent the message itself. Its body will look like this:

package cz.stechy.chat.model;

public final class ChatMessageEntry {

    private final ChatContact chatContact;
    private final String message;

    ChatMessageEntry(ChatContact chatContact, String message) {
        this.chatContact = chatContact;
        this.message = message;
    }

    public ChatContact getChatContact() {
        return chatContact;
    }

    public String getMessage() {
        return message;
    }
}

The class contains only two properties: chatContact and message.

ChatService

Now let's create an interface that will define the methods for chat:

public interface IChatService {
    void saveUserId(String id);
    void sendMessage(String id, String message);
    void notifyTyping(String id, boolean typing);
    ObservableMap <String, ChatContact> getClients();
}

The interface defines the most basic functions. The setUserId() method stores the ID of the user who logged in to the server. sendMessage() will send a message. Using the notifyTyping() method, we'll notify the other party that we've started writing a message. The getClients() method returns an observable map of all logged-in users.

Implementing the Interface

Next to the interface, we'll create a ChatService class to implement it:

package cz.stechy.chat.service;

public final class ChatService implements IChatService {

    private final ObservableMap <String, ChatContact> clients = FXCollections.observableHashMap();
    private final List <String> typingInformations = new ArrayList<>();
    private final IClientCommunicationService communicator;
    private String thisUserId;

    public ChatService(IClientCommunicationService communicator) {
        this.communicator = communicator;
        this.communicator.connectionStateProperty().addListener((observable, oldValue, newValue) -> {
            switch (newValue) {
                case CONNECTED:
                    this.communicator.registerMessageObserver(ChatMessage.MESSAGE_TYPE, this.chatMessageListener);
                    break;
                case CONNECTING:
                    break;
                case DISCONNECTED:
                    this.communicator.unregisterMessageObserver(ChatMessage.MESSAGE_TYPE, this.chatMessageListener);
                    break;
            }
        });
    }

    private ChatContact getContactById(String id) {
        return clients.get(id);
    }

    @Override
    public void saveUserId(String id) {
        this.thisUserId = id;
    }

    @Override
    public void sendMessage(String id, String message) {
        final ChatContact chatContact = clients.get(id);
        if (chatContact == null) {
            throw new RuntimeException("Client not found.");
        }

        byte[] messageData = (message + " ").getBytes();
        communicator.sendMessage(
            new ChatMessage(
            new ChatMessageCommunicationData(id, messageData)));

        chatContact.addMessage(clients.get(thisUserId), message);
    }

    @Override
    public void notifyTyping(String id, boolean typing) {
        if (typing && typingInformations.contains(id)) {
            return;
        }

        communicator.sendMessage(new ChatMessage(
            new ChatMessageAdministrationData(
            new ChatMessageAdministrationClientTyping(
            typing ? ChatAction.CLIENT_TYPING : ChatAction.CLIENT_NOT_TYPING, id))));

        if (typing) {
            typingInformations.add(id);
        } else {
            typingInformations.remove(id);
        }
    }

    @Override
    public ObservableMap <String, ChatContact> getClients() {
        return clients;
    }

    private final OnDataReceivedListener chatMessageListener = message -> {};
}

The class contains three instance constants:

  • clients - An observable map of all logged-in users
  • typingInformations - A collection of users who are currently writing a message
  • communicator - A service mediating the communication with the server.

We get the communicator in the constructor and store it. Next, we set a listener to changes of the connection state. This is because we want to respond to incoming messages only when we're connected. In the sendMessage() method, we create a new message with the specified content and use the communicator to send it to the server. Next we add this message to the list of "received" messages. The notifyTyping() method is used to inform whether we have informed the user on the other side that we've started / stopped writing. We use the typingInformations register to avoid sending a message every time we type a character. The getClients() method returns an observable map of all logged-in clients. Finally, there's the chatMessageListener variable, which contains an anonymous OnDataReceivedListener() function. We'll now complete this function together.

OnDataReceivedListener

To get started, we'll cast the received message to the ChatMessage class and use the getData() method to get the IChatMessageData interface:

final ChatMessage chatMessage = (ChatMessage) message;
final IChatMessageData messageData = (IChatMessageData) chatMessage.getData();

We'll get the data type from the messageData variable using the getDataType() method. We'll make a switch to decide how to process the data type:

switch (messageData.getDataType()) {
    case DATA_ADMINISTRATION:
        break;
    case DATA_COMMUNICATION:
        break;
    default:
        throw new IllegalArgumentException("Invalid parameter.");
}

The getDataType() method returns one of two values in the ChatMessageDataType enumeration. For administrative data, we'll process messages such as:

  • CLIENT_CONNECTED / CLIENT_DISCONNECTED
  • CLIENT_TYPING / CLIENT_NOT_TYPING

If data of the DATA_COMMUNICATION type arrives, we know there's a message to display.

DATA_ADMINISTRATION

case DATA_ADMINISTRATION:
    final ChatMessageAdministrationData administrationData = (ChatMessageAdministrationData) messageData;
    final IChatMessageAdministrationData data = (IChatMessageAdministrationData) administrationData.getData();
    switch (data.getAction()) {
        case CLIENT_CONNECTED:
            final ChatMessageAdministrationClientState messageAdministrationClientConnected = (ChatMessageAdministrationClientState) data;
            final String connectedClientID = messageAdministrationClientConnected.getId();
            final String connectedClientName = messageAdministrationClientConnected.getName();
            Platform.runLater(() -> clients.putIfAbsent(connectedClientID, new ChatContact(connectedClientID, connectedClientName)));
            break;
        case CLIENT_DISCONNECTED:
            final ChatMessageAdministrationClientState messageAdministrationClientDiconnected = (ChatMessageAdministrationClientState) data;
            final String disconnectedClientID = messageAdministrationClientDiconnected.getId();
            Platform.runLater(() -> clients.remove(disconnectedClientID));
            break;
        case CLIENT_TYPING:
            final ChatMessageAdministrationClientTyping messageAdministrationClientTyping = (ChatMessageAdministrationClientTyping) data;
            final String typingClientId = messageAdministrationClientTyping.getId();
            final ChatContact typingClient = getContactById(typingClientId);
            Platform.runLater(typingClient::setTyping);
            break;
        case CLIENT_NOT_TYPING:
            final ChatMessageAdministrationClientTyping messageAdministrationClientNoTyping = (ChatMessageAdministrationClientTyping) data;
            final String noTypingClientId = messageAdministrationClientNoTyping.getId();
            final ChatContact noTypingClient = getContactById(noTypingClientId);
            Platform.runLater(noTypingClient::resetTyping);
            break;
        default:
            throw new IllegalArgumentException("Invalid argument.");
    }
    break;

First, we extract information about the administrative data. The getAction() method gets the action that the message represents. Based on this action we decide in the switch how we'll process the message. Most of the code is about getting the data itself. The action to be performed is then called using Platform.runLater().

When a communication message arrives, we'll show it to the user:

case DATA_COMMUNICATION:
    final ChatMessageCommunicationData communicationData = (ChatMessageCommunicationData) messageData;
    final ChatMessageCommunicationDataContent communicationDataContent = (ChatMessageCommunicationDataContent) communicationData.getData();
    final String destination = communicationDataContent.getDestination();
    final byte[] messageRaw = communicationDataContent.getData();
    final String messageContent = new String(messageRaw, StandardCharsets.UTF_8);
    Platform.runLater(() -> {
        if (clients.containsKey(destination)) {
            final ChatContact chatContact = clients.get(destination);
            chatContact.addMessage(chatContact, messageContent);
        }
    });
    break;

Note that we don't care how the message is displayed to the user. We just add a new message to the selected contact. Another layer will take care of the rest.

That'd be all for today's lesson.

Next time, in the lesson Java Chat - Server - Chat Plugin, we'll move to the server again and create a plugin that will take care of communicating with our ChatService.


 

Previous article
Java Chat - Server - User Management
All articles in this section
Server for Client Applications in Java
Skip article
(not recommended)
Java Chat - Server - Chat Plugin
Article has been written for you by Petr Štechmüller
Avatar
User rating:
1 votes
Activities