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:

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ávucommunicator- 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_DISCONNECTEDCLIENT_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 16x (130.04 kB)
Aplikace je včetně zdrojových kódů v jazyce Java
