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_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 switch
i 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