feat: migration complète vers WebSockets Next + Kafka pour temps réel
- Migration de Jakarta WebSocket vers Quarkus WebSockets Next - Implémentation de l'architecture Kafka pour événements temps réel - Ajout des DTOs d'événements (NotificationEvent, ChatMessageEvent, ReactionEvent, PresenceEvent) - Création des bridges Kafka → WebSocket (NotificationKafkaBridge, ChatKafkaBridge, ReactionKafkaBridge) - Mise à jour des services pour publier dans Kafka au lieu d'appeler directement WebSocket - Suppression des classes obsolètes (ChatWebSocket, NotificationWebSocket) - Correction de l'injection des paramètres path dans WebSockets Next (utilisation de connection.pathParam) - Ajout des migrations DB pour bookings, promotions, business hours, amenities, reviews - Mise à jour de la configuration application.properties pour Kafka et WebSockets Next - Mise à jour .gitignore pour ignorer les fichiers de logs
This commit is contained in:
301
src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java
Normal file
301
src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java
Normal file
@@ -0,0 +1,301 @@
|
||||
package com.lions.dev.websocket;
|
||||
|
||||
import com.lions.dev.dto.response.chat.MessageResponseDTO;
|
||||
import com.lions.dev.entity.chat.Message;
|
||||
import com.lions.dev.service.MessageService;
|
||||
import io.quarkus.logging.Log;
|
||||
import io.quarkus.websockets.next.OnClose;
|
||||
import io.quarkus.websockets.next.OnOpen;
|
||||
import io.quarkus.websockets.next.OnTextMessage;
|
||||
import io.quarkus.websockets.next.WebSocket;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* WebSocket endpoint pour le chat en temps réel (WebSockets Next).
|
||||
*
|
||||
* Architecture v2.0:
|
||||
* Client → WebSocket → MessageService → Kafka → Bridge → WebSocket → Destinataire
|
||||
*
|
||||
* Gère:
|
||||
* - La connexion/déconnexion des utilisateurs
|
||||
* - L'envoi et la réception de messages en temps réel
|
||||
* - Les indicateurs de frappe (typing indicators)
|
||||
* - Les confirmations de lecture (read receipts)
|
||||
* - Les confirmations de délivrance
|
||||
*
|
||||
* URL: ws://localhost:8080/chat/{userId}
|
||||
*/
|
||||
@WebSocket(path = "/chat/{userId}")
|
||||
@ApplicationScoped
|
||||
public class ChatWebSocketNext {
|
||||
|
||||
@Inject
|
||||
MessageService messageService;
|
||||
|
||||
// Map pour stocker les sessions WebSocket des utilisateurs connectés
|
||||
private static final Map<UUID, WebSocketConnection> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(WebSocketConnection connection) {
|
||||
String userId = connection.pathParam("userId");
|
||||
try {
|
||||
UUID userUUID = UUID.fromString(userId);
|
||||
sessions.put(userUUID, connection);
|
||||
Log.info("[CHAT-WS-NEXT] WebSocket ouvert pour l'utilisateur ID : " + userId);
|
||||
|
||||
// Envoyer un message de confirmation
|
||||
String confirmation = buildJsonMessage("connected",
|
||||
Map.of("message", "Connecté au chat"));
|
||||
connection.sendText(confirmation);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.error("[CHAT-WS-NEXT] UUID invalide: " + userId, e);
|
||||
connection.close();
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors de la connexion", e);
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(WebSocketConnection connection) {
|
||||
try {
|
||||
String userId = connection.pathParam("userId");
|
||||
UUID userUUID = UUID.fromString(userId);
|
||||
sessions.remove(userUUID);
|
||||
Log.info("[CHAT-WS-NEXT] WebSocket fermé pour l'utilisateur ID : " + userId);
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors de la fermeture", e);
|
||||
}
|
||||
}
|
||||
|
||||
@OnTextMessage
|
||||
public void onMessage(String message, WebSocketConnection connection) {
|
||||
try {
|
||||
String userId = connection.pathParam("userId");
|
||||
Log.debug("[CHAT-WS-NEXT] Message reçu de " + userId + ": " + message);
|
||||
|
||||
// Parser le message JSON
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
Map<String, Object> messageData = mapper.readValue(message, Map.class);
|
||||
|
||||
String type = (String) messageData.get("type");
|
||||
|
||||
switch (type) {
|
||||
case "message":
|
||||
handleChatMessage(messageData, userId);
|
||||
break;
|
||||
case "typing":
|
||||
handleTypingIndicator(messageData, userId);
|
||||
break;
|
||||
case "read":
|
||||
handleReadReceipt(messageData, userId);
|
||||
break;
|
||||
default:
|
||||
Log.warn("[CHAT-WS-NEXT] Type de message inconnu: " + type);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors du traitement du message", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'envoi d'un message de chat.
|
||||
* Le message est traité par MessageService qui publiera dans Kafka.
|
||||
*/
|
||||
private void handleChatMessage(Map<String, Object> messageData, String senderId) {
|
||||
try {
|
||||
UUID senderUUID = UUID.fromString(senderId);
|
||||
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
|
||||
String content = (String) messageData.get("content");
|
||||
String messageType = messageData.getOrDefault("messageType", "text").toString();
|
||||
String mediaUrl = (String) messageData.get("mediaUrl");
|
||||
|
||||
// Enregistrer le message dans la base de données
|
||||
// MessageService publiera automatiquement dans Kafka
|
||||
Message message = messageService.sendMessage(
|
||||
senderUUID,
|
||||
recipientUUID,
|
||||
content,
|
||||
messageType,
|
||||
mediaUrl
|
||||
);
|
||||
|
||||
// Créer le DTO de réponse
|
||||
MessageResponseDTO response = new MessageResponseDTO(message);
|
||||
String responseJson = buildJsonMessage("message",
|
||||
Map.of("message", response));
|
||||
|
||||
// Envoyer confirmation à l'expéditeur
|
||||
sendToUser(senderUUID, responseJson);
|
||||
|
||||
Log.info("[CHAT-WS-NEXT] Message traité de " + senderId + " à " + recipientUUID);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi du message", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère les indicateurs de frappe.
|
||||
*/
|
||||
private void handleTypingIndicator(Map<String, Object> messageData, String userId) {
|
||||
try {
|
||||
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
|
||||
boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false);
|
||||
|
||||
String response = buildJsonMessage("typing", Map.of(
|
||||
"userId", userId,
|
||||
"isTyping", isTyping
|
||||
));
|
||||
|
||||
sendToUser(recipientUUID, response);
|
||||
|
||||
Log.debug("[CHAT-WS-NEXT] Indicateur de frappe envoyé de " + userId + " à " + recipientUUID);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi de l'indicateur de frappe", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère les confirmations de lecture.
|
||||
*/
|
||||
private void handleReadReceipt(Map<String, Object> messageData, String userId) {
|
||||
try {
|
||||
UUID messageUUID = UUID.fromString((String) messageData.get("messageId"));
|
||||
|
||||
// Marquer le message comme lu
|
||||
Message message = messageService.markMessageAsRead(messageUUID);
|
||||
|
||||
if (message != null) {
|
||||
// Envoyer confirmation de lecture à l'expéditeur via WebSocket
|
||||
// (sera aussi publié dans Kafka par MessageService)
|
||||
UUID senderUUID = message.getSender().getId();
|
||||
String response = buildJsonMessage("read_receipt", Map.of(
|
||||
"messageId", messageUUID.toString(),
|
||||
"readBy", userId,
|
||||
"readAt", System.currentTimeMillis()
|
||||
));
|
||||
|
||||
sendToUser(senderUUID, response);
|
||||
Log.info("[CHAT-WS-NEXT] Confirmation de lecture envoyée pour message " + messageUUID);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors du traitement de la confirmation de lecture", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message chat à un utilisateur spécifique via WebSocket.
|
||||
* Appelé par le bridge Kafka → WebSocket.
|
||||
*
|
||||
* @param userId ID de l'utilisateur destinataire
|
||||
* @param message Message JSON à envoyer
|
||||
*/
|
||||
public static void sendMessageToUser(UUID userId, String message) {
|
||||
WebSocketConnection connection = sessions.get(userId);
|
||||
|
||||
if (connection == null || !connection.isOpen()) {
|
||||
Log.debug("[CHAT-WS-NEXT] Utilisateur " + userId + " non connecté");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
connection.sendText(message);
|
||||
Log.debug("[CHAT-WS-NEXT] Message envoyé à l'utilisateur: " + userId);
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une confirmation de délivrance à l'expéditeur via WebSocket.
|
||||
*/
|
||||
public static void sendDeliveryConfirmation(UUID senderId, Map<String, Object> confirmationData) {
|
||||
WebSocketConnection connection = sessions.get(senderId);
|
||||
|
||||
if (connection == null || !connection.isOpen()) {
|
||||
Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String response = buildJsonMessage("delivery_confirmation", confirmationData);
|
||||
connection.sendText(response);
|
||||
Log.debug("[CHAT-WS-NEXT] Confirmation de délivrance envoyée à: " + senderId);
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation à " + senderId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une confirmation de lecture à l'expéditeur via WebSocket.
|
||||
*/
|
||||
public static void sendReadConfirmation(UUID senderId, Map<String, Object> readData) {
|
||||
WebSocketConnection connection = sessions.get(senderId);
|
||||
|
||||
if (connection == null || !connection.isOpen()) {
|
||||
Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation de lecture");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String response = buildJsonMessage("read_confirmation", readData);
|
||||
connection.sendText(response);
|
||||
Log.debug("[CHAT-WS-NEXT] Confirmation de lecture envoyée à: " + senderId);
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation lecture à " + senderId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message à un utilisateur (méthode privée pour usage interne).
|
||||
*/
|
||||
private void sendToUser(UUID userId, String message) {
|
||||
WebSocketConnection connection = sessions.get(userId);
|
||||
|
||||
if (connection != null && connection.isOpen()) {
|
||||
try {
|
||||
connection.sendText(message);
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit un message JSON.
|
||||
*/
|
||||
private static String buildJsonMessage(String type, Map<String, Object> data) {
|
||||
try {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
Map<String, Object> message = Map.of(
|
||||
"type", type,
|
||||
"data", data,
|
||||
"timestamp", System.currentTimeMillis()
|
||||
);
|
||||
return mapper.writeValueAsString(message);
|
||||
} catch (Exception e) {
|
||||
Log.error("[CHAT-WS-NEXT] Erreur construction JSON", e);
|
||||
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction\"}}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le nombre d'utilisateurs connectés au chat.
|
||||
*/
|
||||
public static int getConnectedUsersCount() {
|
||||
return sessions.size();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user