NOVINKA! E-learningové kurzy umělé inteligence. Nyní AI za nejlepší ceny. Zjisti více:
NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!

Lekce 21 - Java chat - Klient - Chat service

V minulé lekci, Java chat - Server - Správa uživatelů, jsme vytvořili správu uživatelů na serveru.

V dnešním Java tutoriálu již začneme tvořit základní stavební kameny pro chat. Nejdříve si vytvoříme třídu, pomocí které budeme posílat zprávy týkající se samotného chatu. Dále navrhneme a implementujeme rozhraní pro chat service, pomocí které budeme přistupovat k funkcím chatu.

Zpráva pro chat

V modulu share, v balíčku message, založíme novou třídu ChatMessage, pomocí které se budou posílat veškeré zprávy, které se budou týkat chatu. Třída je velká, nejprve si uvedeme její zdrojový kód a poté si ji vizualizujeme na diagramu:

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, // Požadavek na připojení k chatovací službě
            CLIENT_CONNECTED,
            CLIENT_DISCONNECTED, // Akce klientů
            CLIENT_TYPING,
            CLIENT_NOT_TYPING, // Informace o tom, zda-li někdo píše
        }

        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;
            }
        }
    }
}

Třídu si pro její velikost vizualizujeme na obrázku:

Diagram tříd pro reprezentaci zprávy chatu - Server pro klientské aplikace v Javě

ChatContact

Nyní založíme třídu, která bude na klientovi reprezentovat jeden kontakt. Třídu nazvěme ChatContact a umístíme jí do balíčku model:

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();
    }
}

Instanční konstanta messages obsahuje kolekci všech zpráv, které si uživatel napsal s daným kontaktem. Smysl ostatních konstant by měl být odvoditelný z jejich názvu. BooleanProperty typing bude indikovat, zda-li kontakt v aktuální chvíli píše zprávu, či nikoliv. Metodou addMessage() přidáme novou zprávu do kolekce zpráv. Metodami setTyping() a resetTyping() budeme nastavovat, zda-li kontakt píše, či nikoliv. Další gettery a settery tu nebudu vypisovat.

Ve třídě jsme použili dosud nevytvořenou třídu ChatMessageEntry, pojďme ji přidat.

ChatMessageEntry

Tato třída bude reprezentovat samotnou zprávu. Její tělo bude vypadat takto:

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;
    }
}

Třída obsahuje pouze dvě vlastnosti: chatContact a message.

ChatService

Nyní si vytvoříme rozhraní, které bude definovat metody pro chat.

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

Rozhraní definuje nejzákladnější funkce. Metoda setUserId() slouží k uložení Id uživatele, který se přihlásil k serveru. Metodou sendMessage() budeme odesílat zprávu. Metodou notifyTyping() budeme oznamovat protější straně, že jsme začali psát. Metoda getClients() vrátí pozorovatelnou mapu všech přilhášených uživatelů.

Implementace rozhraní

Vedle rozhraní vytvoříme třídu ChatService, která implementuje výše zmíněné rozhraní:

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("Klient nebyl nalezen.");
        }

        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 -> {};
}

Třída obsahuje tři instanční konstanty:

  • clients - pozorovatelná mapa všech přihlášených uživatelů
  • typingInformations - kolekce uživatelů, kteří právě píší nějakou zprávu
  • communicator - služba zprostředkovávající komunikaci se serverem.

V konstruktoru získáme komunikátor a jeho referenci si uložíme. Dále nastavíme listener na změnu stavu připojení. Chceme totiž reagovat na příchozí zprávy pouze v případě, že jsme připojeni. V metodě sendMessage() vytvoříme novou zprávu se zadaným obsahem a pomocí komunikátoru ji odešleme na server. Dále tuto zprávu přidáme do seznamu "přijatých" zpráv. Metoda notifyTyping() slouží k informování, zda-li jsme my informovali uživatele na druhé straně, že jsme začali/přestali psát. Využíváme tu právě registr typingInformations, abychom neposílali zprávu pokaždé, když napíšeme znak. Metodou getClients() vracíme pozorovatelnou mapu všech přihlášených klientů. Nakonec zbývá proměnná chatMessageListener, která obsahuje anonymní funkci typu OnDataReceivedListener(). Tuto funkci teď spolu vyplníme.

OnDataReceivedListener

Začneme tím, že přetypujeme přijatou zprávu na třídu ChatMessage a metodou getData() získáme rozhraní IChatMessageData:

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

Z proměnné messageData získáme metodou getDataType() typ dat. Nad tímto typem dat uděláme switch, pomocí kterého se rozhodneme, jak data zpracovat:

switch (messageData.getDataType()) {
    case DATA_ADMINISTRATION:
        break;
    case DATA_COMMUNICATION:
        break;
    default:
        throw new IllegalArgumentException("Neplatný parametr.");
}

Metoda getDataType() vrátí jednu ze dvou hodnot ve výčtu ChatMessageDataType. Pokud se bude jednat o administrativní data, budeme zpracovávat zprávy typu:

  • CLIENT_CONNECTED/CLIENT_DISCONNECTED
  • CLIENT_TYPING/CLIENT_NOT_TYPING

Pokud příjdou data typu DATA_COMMUNICATION, víme, že přišla zpráva, kterou je třeba zobrazit.

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("Neplatny argument.");
    }
    break;

Nejdříve vytáhneme informace o administrativních datech. Metodou getAction() získáme akci, kterou zpráva představuje. Na základě této akce se ve switchi rozhodneme, jak budeme zprávu zpracovávat. Většinu kódu zabere rozbalení vlastních dat. Samotná akce, která se má provést, je poté volána pomocí Platform.runLater().

Když přijde komunikační zpráva, zobrazíme ji uživateli:

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;

Všimněte si, že se vůbec nestaráme, jak se zpráva uživateli zobrazí. Pouze přidáme novou zprávu vybranému kontaktu. O zbytek se postará jiná vrstva.

To by bylo pro dnešní lekci vše.

Příště, v lekci Java chat - Server - Chat plugin, se opět přesuneme na server a vytvoříme plugin, který se bude starat o komunikaci s naší ChatService.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 15x (130.04 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

Předchozí článek
Java chat - Server - Správa uživatelů
Všechny články v sekci
Server pro klientské aplikace v Javě
Přeskočit článek
(nedoporučujeme)
Java chat - Server - Chat plugin
Článek pro vás napsal Petr Štechmüller
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity