payloadMap = objectMapper.readValue(json, Map.class);
+ handler.accept(payloadMap);
+ } catch (Exception e) {
+ log.error("[Messaging] Error parsing payload: {}", e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/de/assecutor/votianlt/messaging/MessagingPublisher.java b/src/main/java/de/assecutor/votianlt/messaging/MessagingPublisher.java
new file mode 100644
index 0000000..d9119d0
--- /dev/null
+++ b/src/main/java/de/assecutor/votianlt/messaging/MessagingPublisher.java
@@ -0,0 +1,43 @@
+package de.assecutor.votianlt.messaging;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Publishing helper to send JSON payloads to clients via WebSocket.
+ */
+public interface MessagingPublisher {
+ void publishAsJson(String clientId, String messageType, Object payload);
+}
+
+@Component
+@Slf4j
+class MessagingPublisherImpl implements MessagingPublisher {
+
+ private final WebSocketService webSocketService;
+ private final ObjectMapper objectMapper;
+
+ public MessagingPublisherImpl(WebSocketService webSocketService, ObjectMapper objectMapper) {
+ this.webSocketService = webSocketService;
+ this.objectMapper = objectMapper;
+ }
+
+ @Override
+ public void publishAsJson(String clientId, String messageType, Object payload) {
+ try {
+ String json = objectMapper.writeValueAsString(payload);
+ byte[] data = json.getBytes(StandardCharsets.UTF_8);
+
+ webSocketService.sendToClient(clientId, messageType, data).exceptionally(ex -> {
+ log.error("[Messaging] Failed to deliver to {}/{}: {}", clientId, messageType, ex.getMessage());
+ return null;
+ });
+
+ } catch (Exception e) {
+ log.error("[Messaging] Failed to publish to {}/{}: {}", clientId, messageType, e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/de/assecutor/votianlt/messaging/WebSocketConfig.java b/src/main/java/de/assecutor/votianlt/messaging/WebSocketConfig.java
new file mode 100644
index 0000000..7ae45f7
--- /dev/null
+++ b/src/main/java/de/assecutor/votianlt/messaging/WebSocketConfig.java
@@ -0,0 +1,35 @@
+package de.assecutor.votianlt.messaging;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
+
+/**
+ * WebSocket configuration that registers the WebSocketService as a handler on
+ * the configured endpoint.
+ */
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig implements WebSocketConfigurer {
+
+ private final WebSocketService webSocketService;
+
+ @Value("${app.messaging.websocket.path:/ws/messaging}")
+ private String wsPath;
+
+ @Value("${app.messaging.websocket.allowed-origins:*}")
+ private String allowedOrigins;
+
+ public WebSocketConfig(WebSocketService webSocketService) {
+ this.webSocketService = webSocketService;
+ }
+
+ @Override
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+ registry.addHandler(webSocketService, wsPath).setAllowedOrigins(allowedOrigins.split(","))
+ .addInterceptors(new HttpSessionHandshakeInterceptor());
+ }
+}
diff --git a/src/main/java/de/assecutor/votianlt/messaging/WebSocketService.java b/src/main/java/de/assecutor/votianlt/messaging/WebSocketService.java
new file mode 100644
index 0000000..fcb9aed
--- /dev/null
+++ b/src/main/java/de/assecutor/votianlt/messaging/WebSocketService.java
@@ -0,0 +1,384 @@
+package de.assecutor.votianlt.messaging;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.*;
+
+/**
+ * WebSocket service for direct bidirectional communication with mobile clients.
+ *
+ * Wire Protocol: Each WebSocket message is a JSON document with a "topic" and
+ * "payload" field:
+ *
+ *
+ * {
+ * "topic": "/server/login",
+ * "payload": { ... }
+ * }
+ *
+ *
+ * Topic Structure:
+ *
+ * - Server to Client: /client/{messageType}
+ * - Client to Server: /server/{messageType}
+ * - Login (special): /server/login (unauthenticated)
+ *
+ */
+@Component
+@Slf4j
+public class WebSocketService extends TextWebSocketHandler {
+
+ @FunctionalInterface
+ public interface MessageHandler {
+ void onMessageReceived(String clientId, byte[] payload);
+ }
+
+ private static final String TOPIC_TO_CLIENT = "/client/%s";
+ private static final long PENDING_SESSION_TIMEOUT_MS = 30_000;
+
+ private final ObjectMapper objectMapper;
+
+ // appUserId -> WebSocketSession
+ private final ConcurrentHashMap clientSessions = new ConcurrentHashMap<>();
+
+ // sessionId -> appUserId (reverse lookup for cleanup on disconnect)
+ private final ConcurrentHashMap sessionToClient = new ConcurrentHashMap<>();
+
+ // sessionId -> PendingSession (connected but not yet logged in)
+ private final ConcurrentHashMap pendingSessions = new ConcurrentHashMap<>();
+
+ private final Map messageHandlers = new ConcurrentHashMap<>();
+ private volatile boolean initialized = false;
+
+ private ScheduledExecutorService pendingSessionCleanup;
+
+ public WebSocketService(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ }
+
+ // ==========================================
+ // Lifecycle
+ // ==========================================
+
+ @PostConstruct
+ public void init() {
+ pendingSessionCleanup = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "ws-pending-cleanup");
+ t.setDaemon(true);
+ return t;
+ });
+ pendingSessionCleanup.scheduleAtFixedRate(this::cleanupPendingSessions, 30, 30, TimeUnit.SECONDS);
+
+ initialized = true;
+ log.info("[WebSocket] Service initialized on endpoint /ws/messaging");
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ if (pendingSessionCleanup != null) {
+ pendingSessionCleanup.shutdownNow();
+ }
+
+ for (var entry : clientSessions.entrySet()) {
+ try {
+ WebSocketSession session = entry.getValue();
+ if (session.isOpen()) {
+ session.close(CloseStatus.GOING_AWAY);
+ }
+ } catch (Exception e) {
+ log.warn("[WebSocket] Error closing session for client {}: {}", entry.getKey(), e.getMessage());
+ }
+ }
+
+ for (var entry : pendingSessions.entrySet()) {
+ try {
+ if (entry.getValue().session.isOpen()) {
+ entry.getValue().session.close(CloseStatus.GOING_AWAY);
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ clientSessions.clear();
+ sessionToClient.clear();
+ pendingSessions.clear();
+ messageHandlers.clear();
+ initialized = false;
+
+ log.info("[WebSocket] Service shut down");
+ }
+
+ // ==========================================
+ // Public API
+ // ==========================================
+
+ public CompletableFuture sendToClient(String clientId, String messageType, byte[] payload) {
+ WebSocketSession session = clientSessions.get(clientId);
+ if (session == null || !session.isOpen()) {
+ return CompletableFuture
+ .failedFuture(new IOException("No active WebSocket session for client: " + clientId));
+ }
+
+ try {
+ String topic = String.format(TOPIC_TO_CLIENT, messageType);
+ String payloadJson = new String(payload, StandardCharsets.UTF_8);
+
+ ObjectNode wireMessage = objectMapper.createObjectNode();
+ wireMessage.put("topic", topic);
+ wireMessage.set("payload", objectMapper.readTree(payloadJson));
+
+ String wireJson = objectMapper.writeValueAsString(wireMessage);
+ log.info("[WebSocket OUT] {} -> {}", topic, wireJson);
+
+ sendToSession(session, wireJson);
+ return CompletableFuture.completedFuture(null);
+ } catch (Exception e) {
+ log.error("[WebSocket] Failed to send to client {}: {}", clientId, e.getMessage());
+ return CompletableFuture.failedFuture(new IOException("Failed to send WebSocket message", e));
+ }
+ }
+
+ public void registerMessageHandler(String messageType, MessageHandler handler) {
+ messageHandlers.put(messageType, handler);
+ log.debug("[WebSocket] Registered handler for messageType: {}", messageType);
+ }
+
+ public boolean isConnected() {
+ return initialized;
+ }
+
+ public boolean isClientConnected(String clientId) {
+ WebSocketSession session = clientSessions.get(clientId);
+ return session != null && session.isOpen();
+ }
+
+ public int getConnectedClientCount() {
+ return clientSessions.size();
+ }
+
+ // ==========================================
+ // WebSocket handler methods
+ // ==========================================
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) {
+ pendingSessions.put(session.getId(), new PendingSession(session, Instant.now()));
+ log.info("[WebSocket] New connection: sessionId={}, remote={}", session.getId(), session.getRemoteAddress());
+ }
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+ try {
+ String json = message.getPayload();
+ JsonNode wireMessage = objectMapper.readTree(json);
+
+ JsonNode topicNode = wireMessage.get("topic");
+ JsonNode payloadNode = wireMessage.get("payload");
+
+ if (topicNode == null || payloadNode == null) {
+ log.warn("[WebSocket] Invalid message format (missing topic or payload): {}", json);
+ return;
+ }
+
+ String topic = topicNode.asText();
+ byte[] payloadBytes = objectMapper.writeValueAsBytes(payloadNode);
+
+ log.info("[WebSocket IN] {} <- {}", topic, json);
+
+ // Login message (special: unauthenticated)
+ if ("/server/login".equals(topic)) {
+ handleLoginMessage(session, payloadBytes);
+ return;
+ }
+
+ // Regular client message: /server/{messageType}
+ if (topic.startsWith("/server/")) {
+ // Verify session is authenticated
+ String appUserId = sessionToClient.get(session.getId());
+ if (appUserId == null) {
+ log.warn("[WebSocket] Unauthenticated session {} tried to send: {}", session.getId(), topic);
+ return;
+ }
+ handleClientMessage(topic, appUserId, payloadBytes);
+ }
+ } catch (Exception e) {
+ log.error("[WebSocket] Error handling message: {}", e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ String sessionId = session.getId();
+
+ // Remove from pending sessions
+ pendingSessions.remove(sessionId);
+
+ // Remove from authenticated sessions
+ String clientId = sessionToClient.remove(sessionId);
+ if (clientId != null) {
+ clientSessions.remove(clientId, session);
+ log.info("[WebSocket] Client disconnected: clientId={}, reason={}", clientId, status);
+ } else {
+ log.info("[WebSocket] Unauthenticated session closed: sessionId={}", sessionId);
+ }
+ }
+
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable exception) {
+ log.error("[WebSocket] Transport error for session {}: {}", session.getId(), exception.getMessage());
+ }
+
+ // ==========================================
+ // Internal message routing
+ // ==========================================
+
+ private void handleLoginMessage(WebSocketSession session, byte[] payloadBytes) {
+ MessageHandler handler = messageHandlers.get("login");
+ if (handler != null) {
+ handler.onMessageReceived(session.getId(), payloadBytes);
+ }
+ }
+
+ /**
+ * Register a pending session as authenticated under the given appUserId.
+ * Called by MessagingConfig after successful login.
+ */
+ public void registerAuthenticatedSession(String wsSessionId, String appUserId) {
+ PendingSession pending = pendingSessions.get(wsSessionId);
+ if (pending == null) {
+ log.warn("[WebSocket] No pending session for wsSessionId={}", wsSessionId);
+ return;
+ }
+ registerClientSession(appUserId, pending.session());
+ }
+
+ /**
+ * Send a wire-format message directly to a session by its WebSocket sessionId.
+ * Used for sending login responses to pending (not yet authenticated) sessions.
+ */
+ public void sendToSessionById(String wsSessionId, String topic, byte[] payload) {
+ try {
+ // Check pending sessions first
+ PendingSession pending = pendingSessions.get(wsSessionId);
+ WebSocketSession session = pending != null ? pending.session() : null;
+
+ // Fallback: check authenticated sessions via reverse lookup
+ if (session == null) {
+ String appUserId = sessionToClient.get(wsSessionId);
+ if (appUserId != null) {
+ session = clientSessions.get(appUserId);
+ }
+ }
+
+ if (session == null || !session.isOpen()) {
+ log.warn("[WebSocket] Cannot send to session {}: not found or closed", wsSessionId);
+ return;
+ }
+
+ String payloadJson = new String(payload, StandardCharsets.UTF_8);
+ ObjectNode wireMessage = objectMapper.createObjectNode();
+ wireMessage.put("topic", topic);
+ wireMessage.set("payload", objectMapper.readTree(payloadJson));
+
+ String wireJson = objectMapper.writeValueAsString(wireMessage);
+ log.info("[WebSocket OUT] {} -> {}", topic, wireJson);
+
+ sendToSession(session, wireJson);
+ } catch (Exception e) {
+ log.error("[WebSocket] Error sending to session {}: {}", wsSessionId, e.getMessage());
+ }
+ }
+
+ private void handleClientMessage(String topic, String appUserId, byte[] payload) {
+ String[] parts = topic.split("/");
+
+ // Handle /server/{messageType} where messageType can contain slashes
+ if (parts.length >= 3) {
+ String messageType = String.join("/", Arrays.copyOfRange(parts, 2, parts.length));
+
+ MessageHandler handler = messageHandlers.get(messageType);
+ if (handler != null) {
+ handler.onMessageReceived(appUserId, payload);
+ } else {
+ log.warn("[WebSocket] No handler registered for messageType: {}", messageType);
+ }
+ }
+ }
+
+ // ==========================================
+ // Session management
+ // ==========================================
+
+ private void registerClientSession(String clientId, WebSocketSession session) {
+ // Close old session if same clientId reconnects
+ WebSocketSession oldSession = clientSessions.put(clientId, session);
+ if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
+ try {
+ String oldSessionId = oldSession.getId();
+ sessionToClient.remove(oldSessionId);
+ oldSession.close(CloseStatus.NORMAL.withReason("Replaced by new connection"));
+ log.info("[WebSocket] Closed old session for clientId={} (replaced)", clientId);
+ } catch (IOException e) {
+ log.warn("[WebSocket] Error closing old session for client {}: {}", clientId, e.getMessage());
+ }
+ }
+
+ sessionToClient.put(session.getId(), clientId);
+ pendingSessions.remove(session.getId());
+
+ log.info("[WebSocket] Client registered: clientId={}, sessionId={}", clientId, session.getId());
+ }
+
+ private void cleanupPendingSessions() {
+ Instant cutoff = Instant.now().minusMillis(PENDING_SESSION_TIMEOUT_MS);
+ pendingSessions.entrySet().removeIf(entry -> {
+ if (entry.getValue().connectedAt.isBefore(cutoff)) {
+ try {
+ WebSocketSession session = entry.getValue().session;
+ if (session.isOpen()) {
+ session.close(CloseStatus.POLICY_VIOLATION.withReason("Login timeout"));
+ }
+ log.info("[WebSocket] Closed pending session (login timeout): sessionId={}", entry.getKey());
+ } catch (IOException e) {
+ log.warn("[WebSocket] Error closing pending session: {}", e.getMessage());
+ }
+ return true;
+ }
+ return false;
+ });
+ }
+
+ // ==========================================
+ // Utility methods
+ // ==========================================
+
+ private void sendToSession(WebSocketSession session, String message) throws IOException {
+ synchronized (session) {
+ if (session.isOpen()) {
+ session.sendMessage(new TextMessage(message));
+ }
+ }
+ }
+
+ // ==========================================
+ // Internal types
+ // ==========================================
+
+ private record PendingSession(WebSocketSession session, Instant connectedAt) {
+ }
+}
diff --git a/src/main/java/de/assecutor/votianlt/messaging/config/PluginMessagingConfig.java b/src/main/java/de/assecutor/votianlt/messaging/config/PluginMessagingConfig.java
deleted file mode 100644
index f31a5b5..0000000
--- a/src/main/java/de/assecutor/votianlt/messaging/config/PluginMessagingConfig.java
+++ /dev/null
@@ -1,242 +0,0 @@
-package de.assecutor.votianlt.messaging.config;
-
-import de.assecutor.votianlt.controller.MessageController;
-import de.assecutor.votianlt.dto.AppLoginRequest;
-import de.assecutor.votianlt.messaging.delivery.MessageDeliveryService;
-import de.assecutor.votianlt.messaging.model.AcknowledgmentMessage;
-import de.assecutor.votianlt.messaging.model.MessageEnvelope;
-import de.assecutor.votianlt.messaging.plugin.*;
-import de.assecutor.votianlt.messaging.plugin.mqtt.MqttMessagingPlugin;
-import de.assecutor.votianlt.service.ClientConnectionService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.context.event.ApplicationReadyEvent;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.event.EventListener;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Map;
-
-/**
- * Configuration for the plugin-based messaging system. Initializes the selected
- * plugin and sets up message routing.
- */
-@Configuration
-@Slf4j
-public class PluginMessagingConfig {
-
- @Value("${app.messaging.plugin.type:mqtt}")
- private String pluginType;
-
- @Value("${app.messaging.plugin.mqtt.broker.host:mqtt-2.assecutor.de}")
- private String mqttBrokerHost;
-
- @Value("${app.messaging.plugin.mqtt.broker.port:1883}")
- private int mqttBrokerPort;
-
- @Value("${app.messaging.plugin.mqtt.username:app}")
- private String mqttUsername;
-
- @Value("${app.messaging.plugin.mqtt.password:apppwd}")
- private String mqttPassword;
-
- @Value("${app.messaging.plugin.mqtt.client.id:votianlt-server}")
- private String mqttClientId;
-
- private final PluginManager pluginManager;
- private final ObjectMapper objectMapper;
-
- public PluginMessagingConfig(PluginManager pluginManager, ObjectMapper objectMapper) {
- this.pluginManager = pluginManager;
- this.objectMapper = objectMapper;
- }
-
- /**
- * Initialize the messaging plugin after application startup. This method is
- * called after all beans are created, so we can safely access
- * MessageDeliveryService.
- */
- @EventListener(ApplicationReadyEvent.class)
- public void initializePlugin(ApplicationReadyEvent event) {
- try {
- MessagingPlugin plugin = createPlugin(pluginType);
- PluginConfig config = createPluginConfig(pluginType);
-
- // Get beans from context (after all beans are created)
- MessageDeliveryService deliveryService = event.getApplicationContext()
- .getBean(MessageDeliveryService.class);
- MessageController messageController = event.getApplicationContext().getBean(MessageController.class);
- ClientConnectionService clientConnectionService = event.getApplicationContext()
- .getBean(ClientConnectionService.class);
-
- // Set up a listener to subscribe when connected
- pluginManager.addStateListener(stateEvent -> {
- if (stateEvent.isConnected()) {
- try {
- setupSubscriptions(deliveryService, messageController, clientConnectionService);
- } catch (Exception e) {
- log.error("[MQTT] Error setting up subscriptions: {}", e.getMessage());
- }
- } else if (stateEvent.getState() == ConnectionStateEvent.ConnectionState.DISCONNECTED) {
- log.info("[MQTT] Disconnected from broker");
- } else if (stateEvent.getState() == ConnectionStateEvent.ConnectionState.ERROR) {
- log.error("[MQTT] Connection error: {}", stateEvent.getErrorMessage());
- }
- });
-
- // Activate plugin
- pluginManager.activatePlugin(plugin, config);
-
- } catch (Exception e) {
- log.error("[MQTT] Failed to initialize: {}", e.getMessage());
- throw new RuntimeException("Failed to initialize messaging plugin", e);
- }
- }
-
- /**
- * Create a plugin instance based on the plugin type.
- */
- private MessagingPlugin createPlugin(String type) {
- return switch (type.toLowerCase()) {
- case "mqtt" -> new MqttMessagingPlugin();
- // Add more plugin types here in the future
- // case "websocket" -> new WebSocketMessagingPlugin();
- // case "grpc" -> new GrpcMessagingPlugin();
- default -> throw new IllegalArgumentException("Unknown plugin type: " + type);
- };
- }
-
- /**
- * Create plugin configuration based on the plugin type.
- */
- private PluginConfig createPluginConfig(String type) {
- PluginConfig config = new PluginConfig();
-
- switch (type.toLowerCase()) {
- case "mqtt" -> {
- config.setProperty("broker.host", mqttBrokerHost);
- config.setProperty("broker.port", mqttBrokerPort);
- config.setProperty("username", mqttUsername);
- config.setProperty("password", mqttPassword);
- config.setProperty("client.id", mqttClientId);
- config.setProperty("auto.reconnect", true);
- config.setProperty("clean.start", true);
- }
- // Add more plugin configurations here
- default -> throw new IllegalArgumentException("Unknown plugin type: " + type);
- }
-
- return config;
- }
-
- /**
- * Setup message subscriptions using the new plugin API.
- */
- private void setupSubscriptions(MessageDeliveryService deliveryService, MessageController messageController,
- ClientConnectionService clientConnectionService) {
- try {
- // Register ACK handler
- pluginManager.registerAckHandler((messageId, payload) -> {
- try {
- String json = new String(payload, StandardCharsets.UTF_8);
- MessageEnvelope envelope = objectMapper.readValue(json, MessageEnvelope.class);
- AcknowledgmentMessage ack = objectMapper.convertValue(envelope.getPayload(),
- AcknowledgmentMessage.class);
- deliveryService.handleAcknowledgment(ack);
- } catch (Exception e) {
- // Ignore ACK handling errors
- }
- });
-
- // Register message handlers for different message types
- String[] messageTypes = { "task_completed", "jobs/assigned", "message", "login", "pong" };
-
- for (String messageType : messageTypes) {
- pluginManager.registerMessageHandler(messageType,
- (clientId, payload) -> handleEnvelopedMessage(clientId, payload, deliveryService,
- messageController, clientConnectionService));
- }
-
- } catch (Exception e) {
- log.error("[MQTT] Error setting up subscriptions: {}", e.getMessage());
- throw new RuntimeException("Failed to setup subscriptions", e);
- }
- }
-
- /**
- * Handle incoming enveloped message. Supports both new envelope format and
- * legacy format for backwards compatibility.
- */
- private void handleEnvelopedMessage(String clientId, byte[] payload, MessageDeliveryService deliveryService,
- MessageController messageController, ClientConnectionService clientConnectionService) {
- try {
- String json = new String(payload, StandardCharsets.UTF_8);
-
- // Try to parse as envelope first
- try {
- MessageEnvelope envelope = objectMapper.readValue(json, MessageEnvelope.class);
- if (envelope.getMessageId() != null && envelope.getTopic() != null) {
- deliveryService.handleIncomingMessage(envelope);
- return;
- }
- } catch (Exception e) {
- // Not a valid envelope, try legacy format
- }
-
- // Handle legacy format (direct payload without envelope)
- handleLegacyMessage(clientId, json, messageController, clientConnectionService);
-
- } catch (Exception e) {
- // Ignore message handling errors
- }
- }
-
- /**
- * Handle legacy message format (without envelope wrapper). This supports older
- * clients that don't use the envelope format.
- */
- @SuppressWarnings("unchecked")
- private void handleLegacyMessage(String clientId, String json, MessageController messageController,
- ClientConnectionService clientConnectionService) {
- try {
- Map payload = objectMapper.readValue(json, Map.class);
-
- // Check if this is a login request (has email, password, clientId)
- if (payload.containsKey("email") && payload.containsKey("password") && payload.containsKey("clientId")) {
- AppLoginRequest loginRequest = objectMapper.convertValue(payload, AppLoginRequest.class);
- messageController.handleAppLogin(loginRequest);
- return;
- }
-
- // Check if this is a pong response
- if ("pong".equals(payload.get("type"))) {
- String pongClientId = clientId != null ? clientId : (String) payload.get("clientId");
- if (pongClientId != null) {
- clientConnectionService.handlePong(pongClientId);
- }
- return;
- }
-
- // Check if this is a task completion
- if (payload.containsKey("taskType") || payload.containsKey("taskId")) {
- String taskType = payload.get("taskType") != null ? payload.get("taskType").toString() : null;
- messageController.handleTaskCompleted(payload, taskType);
- return;
- }
-
- // Check if this is a jobs/assigned request
- if (payload.containsKey("appUserId")) {
- if (clientId != null) {
- payload.put("clientId", clientId);
- }
- messageController.handleGetAssignedJobs(payload);
- return;
- }
-
- } catch (Exception e) {
- // Ignore legacy message handling errors
- }
- }
-}
diff --git a/src/main/java/de/assecutor/votianlt/messaging/delivery/AcknowledgmentHandler.java b/src/main/java/de/assecutor/votianlt/messaging/delivery/AcknowledgmentHandler.java
deleted file mode 100644
index ca04805..0000000
--- a/src/main/java/de/assecutor/votianlt/messaging/delivery/AcknowledgmentHandler.java
+++ /dev/null
@@ -1,147 +0,0 @@
-package de.assecutor.votianlt.messaging.delivery;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import de.assecutor.votianlt.controller.MessageController;
-import de.assecutor.votianlt.dto.AppLoginRequest;
-import de.assecutor.votianlt.messaging.model.MessageEnvelope;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.stereotype.Component;
-
-import java.util.Map;
-
-/**
- * Handles acknowledgments and routes incoming messages to application layer.
- * Acts as a bridge between the messaging layer and the application logic.
- */
-@Component
-@Slf4j
-public class AcknowledgmentHandler {
-
- private final MessageController messageController;
- private final ObjectMapper objectMapper;
-
- public AcknowledgmentHandler(@Lazy MessageController messageController, ObjectMapper objectMapper) {
- this.messageController = messageController;
- this.objectMapper = objectMapper;
- }
-
- /**
- * Route incoming message envelope to appropriate application handler. Unwraps
- * the envelope and delegates to MessageController.
- */
- public void routeIncomingMessage(MessageEnvelope envelope) {
- try {
- String topic = envelope.getTopic();
- Object payload = envelope.getPayload();
-
- log.debug("[AckHandler] Routing message {} on topic {}", envelope.getMessageId(), topic);
-
- // Convert payload to Map for routing
- Map payloadMap = objectMapper.convertValue(payload,
- new TypeReference