Erweiterungen

This commit is contained in:
2025-12-15 11:30:49 +01:00
parent c0085d8931
commit 9cf2e9b590
8 changed files with 118 additions and 36 deletions

View File

@@ -0,0 +1,31 @@
package de.assecutor.votianlt.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Jackson configuration for consistent JSON serialization across the application.
* Ensures all date/time fields are serialized as ISO 8601 strings.
*/
@Configuration
public class JacksonConfig {
/**
* Creates a configured ObjectMapper bean that serializes dates as ISO 8601 strings.
* This bean is used throughout the application for JSON serialization.
*/
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
// Serialize dates as ISO 8601 strings instead of timestamps/arrays
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return objectMapper;
}
}

View File

@@ -28,7 +28,6 @@ import de.assecutor.votianlt.service.MessageService;
import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.model.JobStatus;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import de.assecutor.votianlt.mqtt.MqttPublisher; import de.assecutor.votianlt.mqtt.MqttPublisher;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -72,12 +71,13 @@ public class MessageController {
private final EmailService emailService; private final EmailService emailService;
private final MessageService messageService; private final MessageService messageService;
private final UserService userService; private final UserService userService;
private final ObjectMapper objectMapper;
public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository, public MessageController(MqttPublisher mqttPublisher, AppUserRepository appUserRepository,
AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository, AppUserService appUserService, JobRepository jobRepository, CargoItemRepository cargoItemRepository,
TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository, TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
SignatureRepository signatureRepository, CommentRepository commentRepository, JobHistoryService jobHistoryService, SignatureRepository signatureRepository, CommentRepository commentRepository, JobHistoryService jobHistoryService,
EmailService emailService, MessageService messageService, UserService userService) { EmailService emailService, MessageService messageService, UserService userService, ObjectMapper objectMapper) {
this.mqttPublisher = mqttPublisher; this.mqttPublisher = mqttPublisher;
this.appUserRepository = appUserRepository; this.appUserRepository = appUserRepository;
this.appUserService = appUserService; this.appUserService = appUserService;
@@ -92,6 +92,7 @@ public class MessageController {
this.emailService = emailService; this.emailService = emailService;
this.messageService = messageService; this.messageService = messageService;
this.userService = userService; this.userService = userService;
this.objectMapper = objectMapper;
} }
/** /**
@@ -199,8 +200,6 @@ public class MessageController {
// Log complete JSON for debugging // Log complete JSON for debugging
log.debug("About to serialize {} jobs to JSON for logging", jobsWithRelatedData.size()); log.debug("About to serialize {} jobs to JSON for logging", jobsWithRelatedData.size());
try { try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
String jsonOutput = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jobsWithRelatedData); String jsonOutput = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jobsWithRelatedData);
log.info("=== COMPLETE JSON RESPONSE FOR MQTT CLIENT ==="); log.info("=== COMPLETE JSON RESPONSE FOR MQTT CLIENT ===");
log.info("AppUserId: {}", appUserId); log.info("AppUserId: {}", appUserId);

View File

@@ -12,7 +12,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -45,10 +44,9 @@ public class PluginMessagingConfig {
private final PluginManager pluginManager; private final PluginManager pluginManager;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public PluginMessagingConfig(PluginManager pluginManager) { public PluginMessagingConfig(PluginManager pluginManager, ObjectMapper objectMapper) {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.objectMapper = new ObjectMapper(); this.objectMapper = objectMapper;
this.objectMapper.registerModule(new JavaTimeModule());
} }
/** /**
@@ -143,7 +141,11 @@ public class PluginMessagingConfig {
pluginManager.registerAckHandler((messageId, payload) -> { pluginManager.registerAckHandler((messageId, payload) -> {
try { try {
String json = new String(payload, StandardCharsets.UTF_8); String json = new String(payload, StandardCharsets.UTF_8);
AcknowledgmentMessage ack = objectMapper.readValue(json, AcknowledgmentMessage.class); log.info("[PluginMessagingConfig] Received ACK JSON: {}", json);
// ACK messages are wrapped in MessageEnvelope
MessageEnvelope envelope = objectMapper.readValue(json, MessageEnvelope.class);
AcknowledgmentMessage ack = objectMapper.convertValue(envelope.getPayload(), AcknowledgmentMessage.class);
deliveryService.handleAcknowledgment(ack); deliveryService.handleAcknowledgment(ack);
} catch (Exception e) { } catch (Exception e) {
log.error("[PluginMessagingConfig] Error handling ACK message: {}", e.getMessage(), e); log.error("[PluginMessagingConfig] Error handling ACK message: {}", e.getMessage(), e);
@@ -177,6 +179,7 @@ public class PluginMessagingConfig {
private void handleEnvelopedMessage(String clientId, byte[] payload, MessageDeliveryService deliveryService) { private void handleEnvelopedMessage(String clientId, byte[] payload, MessageDeliveryService deliveryService) {
try { try {
String json = new String(payload, StandardCharsets.UTF_8); String json = new String(payload, StandardCharsets.UTF_8);
log.info("[PluginMessagingConfig] Received JSON from client {}: {}", clientId, json);
// Try to parse as envelope first // Try to parse as envelope first
try { try {
@@ -184,7 +187,7 @@ public class PluginMessagingConfig {
deliveryService.handleIncomingMessage(envelope); deliveryService.handleIncomingMessage(envelope);
} catch (Exception e) { } catch (Exception e) {
// If not an envelope, it might be a legacy message - log and skip // If not an envelope, it might be a legacy message - log and skip
log.debug("[PluginMessagingConfig] Received non-enveloped message from client {}, skipping", clientId); log.warn("[PluginMessagingConfig] Received non-enveloped message from client {}, skipping. JSON: {}", clientId, json);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[PluginMessagingConfig] Error handling enveloped message from client {}: {}", clientId, e.getMessage(), e); log.error("[PluginMessagingConfig] Error handling enveloped message from client {}: {}", clientId, e.getMessage(), e);

View File

@@ -2,7 +2,6 @@ package de.assecutor.votianlt.messaging.delivery;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import de.assecutor.votianlt.controller.MessageController; import de.assecutor.votianlt.controller.MessageController;
import de.assecutor.votianlt.dto.AppLoginRequest; import de.assecutor.votianlt.dto.AppLoginRequest;
import de.assecutor.votianlt.messaging.model.MessageEnvelope; import de.assecutor.votianlt.messaging.model.MessageEnvelope;
@@ -23,10 +22,9 @@ public class AcknowledgmentHandler {
private final MessageController messageController; private final MessageController messageController;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public AcknowledgmentHandler(@Lazy MessageController messageController) { public AcknowledgmentHandler(@Lazy MessageController messageController, ObjectMapper objectMapper) {
this.messageController = messageController; this.messageController = messageController;
this.objectMapper = new ObjectMapper(); this.objectMapper = objectMapper;
this.objectMapper.registerModule(new JavaTimeModule());
} }
/** /**

View File

@@ -1,7 +1,6 @@
package de.assecutor.votianlt.messaging.delivery; package de.assecutor.votianlt.messaging.delivery;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import de.assecutor.votianlt.messaging.model.*; import de.assecutor.votianlt.messaging.model.*;
import de.assecutor.votianlt.messaging.plugin.PluginManager; import de.assecutor.votianlt.messaging.plugin.PluginManager;
import de.assecutor.votianlt.messaging.plugin.SendOptions; import de.assecutor.votianlt.messaging.plugin.SendOptions;
@@ -36,14 +35,14 @@ public class MessageDeliveryServiceImpl implements MessageDeliveryService {
PendingDeliveryRepository pendingDeliveryRepository, PendingDeliveryRepository pendingDeliveryRepository,
MessageEnvelopeRepository envelopeRepository, MessageEnvelopeRepository envelopeRepository,
AcknowledgmentHandler acknowledgmentHandler, AcknowledgmentHandler acknowledgmentHandler,
DeliveryConfig config) { DeliveryConfig config,
ObjectMapper objectMapper) {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.pendingDeliveryRepository = pendingDeliveryRepository; this.pendingDeliveryRepository = pendingDeliveryRepository;
this.envelopeRepository = envelopeRepository; this.envelopeRepository = envelopeRepository;
this.acknowledgmentHandler = acknowledgmentHandler; this.acknowledgmentHandler = acknowledgmentHandler;
this.config = config; this.config = config;
this.objectMapper = new ObjectMapper(); this.objectMapper = objectMapper;
this.objectMapper.registerModule(new JavaTimeModule());
} }
@Override @Override
@@ -64,6 +63,7 @@ public class MessageDeliveryServiceImpl implements MessageDeliveryService {
// Serialize envelope to JSON // Serialize envelope to JSON
String json = objectMapper.writeValueAsString(envelope); String json = objectMapper.writeValueAsString(envelope);
byte[] envelopeData = json.getBytes(StandardCharsets.UTF_8); byte[] envelopeData = json.getBytes(StandardCharsets.UTF_8);
log.info("[MessageDelivery] Sending JSON to client {} (type: {}): {}", clientId, messageType, json);
// Create pending delivery record if acknowledgment is required // Create pending delivery record if acknowledgment is required
if (options.isRequiresAck()) { if (options.isRequiresAck()) {

View File

@@ -21,8 +21,8 @@ import java.util.concurrent.ConcurrentHashMap;
* Topic Structure (managed internally): * Topic Structure (managed internally):
* - Server -> Client: /client/{clientId}/{messageType} * - Server -> Client: /client/{clientId}/{messageType}
* - Client -> Server: /server/{clientId}/{messageType} * - Client -> Server: /server/{clientId}/{messageType}
* - ACK Server -> Client: /client/{clientId}/{messageId}/ack * - ACK Server -> Client: /client/{clientId}/ack (messageId in payload)
* - ACK Client -> Server: /server/{messageId}/ack * - ACK Client -> Server: /server/{clientId}/ack (messageId in payload)
*/ */
@Slf4j @Slf4j
public class MqttMessagingPlugin implements MessagingPlugin { public class MqttMessagingPlugin implements MessagingPlugin {
@@ -33,8 +33,8 @@ public class MqttMessagingPlugin implements MessagingPlugin {
// Topic templates // Topic templates
private static final String TOPIC_TO_CLIENT = "/client/%s/%s"; // /client/{clientId}/{messageType} private static final String TOPIC_TO_CLIENT = "/client/%s/%s"; // /client/{clientId}/{messageType}
private static final String TOPIC_FROM_CLIENT = "/server/%s/%s"; // /server/{clientId}/{messageType} private static final String TOPIC_FROM_CLIENT = "/server/%s/%s"; // /server/{clientId}/{messageType}
private static final String TOPIC_ACK_TO_CLIENT = "/client/%s/%s/ack"; // /client/{clientId}/{messageId}/ack private static final String TOPIC_ACK_TO_CLIENT = "/client/%s/ack"; // /client/{clientId}/ack (messageId in payload)
private static final String TOPIC_ACK_FROM_CLIENT = "/server/%s/ack"; // /server/{messageId}/ack private static final String TOPIC_ACK_FROM_CLIENT = "/server/%s/ack"; // /server/{clientId}/ack (messageId in payload)
// Subscription patterns // Subscription patterns
private static final String PATTERN_FROM_CLIENT = "/server/+/%s"; // /server/+/{messageType} private static final String PATTERN_FROM_CLIENT = "/server/+/%s"; // /server/+/{messageType}
@@ -205,7 +205,7 @@ public class MqttMessagingPlugin implements MessagingPlugin {
return CompletableFuture.failedFuture(new PluginException("MQTT client is not connected")); return CompletableFuture.failedFuture(new PluginException("MQTT client is not connected"));
} }
String topic = String.format(TOPIC_ACK_TO_CLIENT, clientId, messageId); String topic = String.format(TOPIC_ACK_TO_CLIENT, clientId);
log.debug("[MqttPlugin] Sending ACK to client {} for message {} on topic: {}", clientId, messageId, topic); log.debug("[MqttPlugin] Sending ACK to client {} for message {} on topic: {}", clientId, messageId, topic);
return sendToTopic(topic, payload, options); return sendToTopic(topic, payload, options);
@@ -351,7 +351,7 @@ public class MqttMessagingPlugin implements MessagingPlugin {
/** /**
* Handle ACK message from client. * Handle ACK message from client.
* Topic format: /server/{messageId}/ack * Topic format: /server/{clientId}/ack (messageId in payload)
*/ */
private void handleAckMessage(String topic, byte[] payload) { private void handleAckMessage(String topic, byte[] payload) {
if (ackHandler == null) { if (ackHandler == null) {
@@ -359,20 +359,71 @@ public class MqttMessagingPlugin implements MessagingPlugin {
return; return;
} }
// Extract messageId from topic: /server/{messageId}/ack // Extract clientId from topic: /server/{clientId}/ack
String[] parts = topic.split("/"); String[] parts = topic.split("/");
if (parts.length >= 4) { if (parts.length >= 4) {
String messageId = parts[2]; // messageId is at index 2 String clientId = parts[2]; // clientId is at index 2
log.debug("[MqttPlugin] Routing ACK for message: {}", messageId);
ackHandler.onAckReceived(messageId, payload); // Extract messageId from payload
String payloadStr = new String(payload, StandardCharsets.UTF_8);
String messageId = extractMessageIdFromPayload(payloadStr);
if (messageId != null) {
log.debug("[MqttPlugin] Routing ACK for message: {} from client: {}", messageId, clientId);
ackHandler.onAckReceived(messageId, payload);
} else {
log.warn("[MqttPlugin] Could not extract messageId from ACK payload: {}", payloadStr);
}
} else { } else {
log.warn("[MqttPlugin] Invalid ACK topic format: {}", topic); log.warn("[MqttPlugin] Invalid ACK topic format: {}", topic);
} }
} }
/**
* Extract messageId from ACK payload.
* Expected payload format: JSON with "messageId" field, e.g., {"messageId": "abc-123"}
* or plain messageId string.
*/
private String extractMessageIdFromPayload(String payload) {
if (payload == null || payload.isBlank()) {
return null;
}
payload = payload.trim();
// Try to extract from JSON format: {"messageId": "..."}
if (payload.startsWith("{")) {
// Simple JSON parsing for messageId field
int keyIndex = payload.indexOf("\"messageId\"");
if (keyIndex == -1) {
keyIndex = payload.indexOf("'messageId'");
}
if (keyIndex >= 0) {
int colonIndex = payload.indexOf(":", keyIndex);
if (colonIndex >= 0) {
int valueStart = payload.indexOf("\"", colonIndex);
if (valueStart == -1) {
valueStart = payload.indexOf("'", colonIndex);
}
if (valueStart >= 0) {
char quote = payload.charAt(valueStart);
int valueEnd = payload.indexOf(quote, valueStart + 1);
if (valueEnd > valueStart) {
return payload.substring(valueStart + 1, valueEnd);
}
}
}
}
}
// If not JSON, treat the entire payload as the messageId
return payload;
}
/** /**
* Handle client message. * Handle client message.
* Topic format: /server/{clientId}/{messageType} or /server/{messageType} (for login) * Topic format: /server/{clientId}/{messageType} or /server/{messageType} (for login)
* messageType can contain slashes, e.g., "jobs/assigned"
*/ */
private void handleClientMessage(String topic, byte[] payload) { private void handleClientMessage(String topic, byte[] payload) {
// Extract clientId and messageType from topic // Extract clientId and messageType from topic
@@ -391,10 +442,12 @@ public class MqttMessagingPlugin implements MessagingPlugin {
return; return;
} }
// Handle /server/{clientId}/{messageType} // Handle /server/{clientId}/{messageType} where messageType can contain slashes
if (parts.length >= 4) { if (parts.length >= 4) {
String clientId = parts[2]; String clientId = parts[2];
String messageType = parts[3]; // Join all parts from index 3 onwards to form the full messageType
// e.g., /server/clientId/jobs/assigned -> messageType = "jobs/assigned"
String messageType = String.join("/", java.util.Arrays.copyOfRange(parts, 3, parts.length));
ClientMessageHandler handler = messageHandlers.get(messageType); ClientMessageHandler handler = messageHandlers.get(messageType);
if (handler != null) { if (handler != null) {

View File

@@ -1,7 +1,6 @@
package de.assecutor.votianlt.mqtt; package de.assecutor.votianlt.mqtt;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import de.assecutor.votianlt.messaging.delivery.MessageDeliveryService; import de.assecutor.votianlt.messaging.delivery.MessageDeliveryService;
import de.assecutor.votianlt.messaging.model.DeliveryOptions; import de.assecutor.votianlt.messaging.model.DeliveryOptions;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -31,10 +30,9 @@ class MqttPublisherImpl implements MqttPublisher {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final MessageDeliveryService deliveryService; private final MessageDeliveryService deliveryService;
public MqttPublisherImpl(@Lazy MessageDeliveryService deliveryService) { public MqttPublisherImpl(@Lazy MessageDeliveryService deliveryService, ObjectMapper objectMapper) {
this.deliveryService = deliveryService; this.deliveryService = deliveryService;
this.objectMapper = new ObjectMapper(); this.objectMapper = objectMapper;
this.objectMapper.registerModule(new JavaTimeModule());
} }
@Override @Override

View File

@@ -19,9 +19,9 @@ public class JobHistoryService {
private final JobHistoryRepository jobHistoryRepository; private final JobHistoryRepository jobHistoryRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public JobHistoryService(JobHistoryRepository jobHistoryRepository) { public JobHistoryService(JobHistoryRepository jobHistoryRepository, ObjectMapper objectMapper) {
this.jobHistoryRepository = jobHistoryRepository; this.jobHistoryRepository = jobHistoryRepository;
this.objectMapper = new ObjectMapper(); this.objectMapper = objectMapper;
} }
/** /**