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:
dahoud
2026-01-21 13:46:16 +00:00
parent 7dd0969799
commit 93c63fd600
78 changed files with 5019 additions and 1113 deletions

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