Erweiterungen

This commit is contained in:
2025-09-13 22:07:01 +02:00
parent f4656cd193
commit 5adfb9c2db
8 changed files with 801 additions and 347 deletions

View File

@@ -6,6 +6,12 @@ import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBr
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import org.springframework.messaging.converter.MessageConverter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* WebSocket configuration for STOMP messaging.
@@ -13,43 +19,74 @@ import org.springframework.web.socket.server.support.HttpSessionHandshakeInterce
*/
@Configuration
@EnableWebSocketMessageBroker
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Enable a simple memory-based message broker to carry messages back to client
// on destinations prefixed with "/topic" and "/queue"
config.enableSimpleBroker("/topic", "/queue");
// Designate the "/app" prefix for messages that are bound to methods
// annotated with @MessageMapping
config.setApplicationDestinationPrefixes("/app");
// Set user destination prefix for user-specific messages
config.setUserDestinationPrefix("/user");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// Increase message size limits for large payloads (like base64 photos)
registration.taskExecutor().corePoolSize(4);
registration.taskExecutor().maxPoolSize(8);
registration.taskExecutor().keepAliveSeconds(60);
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
// Configure outbound channel for better performance with large messages
registration.taskExecutor().corePoolSize(4);
registration.taskExecutor().maxPoolSize(8);
registration.taskExecutor().keepAliveSeconds(60);
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
// Use framework defaults (no custom large-message settings)
}
@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
// Use default message converters (no custom large-payload converter)
return false; // keep default converters
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
log.info("=== REGISTERING WEBSOCKET ENDPOINTS ===");
// Register the "/ws" endpoint for WebSocket connections with SockJS fallback
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS()
.setHeartbeatTime(25000) // Set heartbeat interval
.setDisconnectDelay(5000) // Set disconnect delay
.setStreamBytesLimit(128 * 1024) // Set stream bytes limit
.setHttpMessageCacheSize(1000) // Set HTTP message cache size
.setSessionCookieNeeded(false); // Disable session cookie requirement
.setHeartbeatTime(25000)
.setDisconnectDelay(5000)
.setSessionCookieNeeded(false);
// Plain WebSocket endpoint without SockJS for native WebSocket clients (Flutter, mobile apps)
registry.addEndpoint("/websocket")
.setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor());
// Additional endpoint specifically for mobile/Flutter clients that might have URL issues
registry.addEndpoint("/stomp")
.setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor());
log.info("WebSocket endpoints registered: /ws (with SockJS), /websocket, /stomp");
}
}

View File

@@ -0,0 +1,47 @@
package de.assecutor.votianlt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.MimeTypeUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.List;
/**
* Additional configuration for handling large WebSocket messages.
* This configuration specifically addresses JSON parsing limits for large payloads.
*/
@Configuration
public class WebSocketMessageSizeConfig {
/**
* Configure Jackson ObjectMapper to handle large JSON strings (like base64 photos).
*/
@Bean("webSocketObjectMapper")
public ObjectMapper webSocketObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
/**
* Configure message converter to use our custom ObjectMapper.
*/
@Bean
public MappingJackson2MessageConverter messageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(webSocketObjectMapper());
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
converter.setContentTypeResolver(resolver);
return converter;
}
}

View File

@@ -6,11 +6,13 @@ import de.assecutor.votianlt.dto.JobWithRelatedDataDTO;
import de.assecutor.votianlt.model.AppUser;
import de.assecutor.votianlt.model.CargoItem;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.Photo;
import de.assecutor.votianlt.model.task.BaseTask;
import de.assecutor.votianlt.pages.service.AppUserService;
import de.assecutor.votianlt.repository.AppUserRepository;
import de.assecutor.votianlt.repository.CargoItemRepository;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.repository.PhotoRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -53,18 +55,22 @@ public class MessageController {
@Autowired
private TaskRepository taskRepository;
@Autowired
private PhotoRepository photoRepository;
/**
* Handles messages sent to /app/message and broadcasts them to all subscribers of /topic/messages
*/
@MessageMapping("/message")
@SendTo("/topic/messages")
public Map<String, Object> handleMessage(Map<String, Object> message) {
log.error("=== ANY MESSAGE RECEIVED === STOMP Endpoint '/app/message' called");
log.info("STOMP Endpoint '/app/message' called with data: {}", message);
// Add timestamp to the message
message.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
message.put("processed", true);
log.info("STOMP Response for '/app/message' sent to '/topic/messages': {}", message);
return message;
}
@@ -234,70 +240,16 @@ public class MessageController {
}
/**
* Report task completion from apps.
* Report generic task completion from apps.
* Client sends to /app/task/completed with payload { taskId, completedBy?, note? }.
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
* This endpoint accepts any task type (fallback for GENERIC or unknown types).
*/
@MessageMapping("/task/completed")
@SendTo("/topic/task-updates")
public Map<String, Object> handleTaskCompleted(Map<String, Object> payload) {
log.info("STOMP Endpoint '/app/task/completed' called with data: {}", payload);
Map<String, Object> response = new java.util.HashMap<>();
response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
response.put("type", "taskCompletedAck");
if (payload == null || !payload.containsKey("taskId") || payload.get("taskId") == null || payload.get("taskId").toString().isBlank()) {
response.put("success", false);
response.put("message", "taskId ist erforderlich");
log.info("Task completion failed: {}", response);
return response;
}
String taskIdStr = payload.get("taskId").toString();
String completedBy = payload.get("completedBy") != null ? payload.get("completedBy").toString() : null;
String note = payload.get("note") != null ? payload.get("note").toString() : null;
try {
org.bson.types.ObjectId taskId = new org.bson.types.ObjectId(taskIdStr);
java.util.Optional<BaseTask> opt = taskRepository.findById(taskId);
if (opt.isEmpty()) {
response.put("success", false);
response.put("message", "Task nicht gefunden");
return response;
}
BaseTask task = opt.get();
task.setCompleted(true);
task.setCompletedAt(LocalDateTime.now());
if (completedBy != null) task.setCompletedBy(completedBy);
if (note != null) task.setCompletionNote(note);
taskRepository.save(task);
java.util.Map<String, Object> event = new java.util.HashMap<>();
event.put("taskId", task.getIdAsString());
event.put("jobId", task.getJobIdAsString());
event.put("completed", task.isCompleted());
event.put("completedAt", task.getCompletedAt() != null ? task.getCompletedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null);
event.put("completedBy", task.getCompletedBy());
event.put("note", task.getCompletionNote());
event.put("event", "taskCompleted");
// Send specific task topic
messagingTemplate.convertAndSend("/topic/tasks/" + task.getIdAsString(), event);
response.put("success", true);
response.putAll(event);
log.info("Task completion processed successfully for taskId={}", taskIdStr);
return response;
} catch (IllegalArgumentException e) {
response.put("success", false);
response.put("message", "Ungültige taskId");
return response;
} catch (Exception e) {
log.error("Error processing task completion", e);
response.put("success", false);
response.put("message", "Fehler bei der Verarbeitung");
return response;
}
return processTaskCompletion(payload, null); // null means accept any task type
}
/**
@@ -314,14 +266,15 @@ public class MessageController {
/**
* Report photo task completion from apps.
* Client sends to /app/task/photo/completed with payload { taskId, completedBy?, note? }.
* Client sends to /app/task/photo/completed with payload { taskId, completedBy?, note?, extraData? }.
* The extraData contains: { photos: base64List, count: base64List.length }
* Broadcasts to /topic/task-updates and /topic/tasks/{taskId}.
*/
@MessageMapping("/task/photo/completed")
@SendTo("/topic/task-updates")
public Map<String, Object> handlePhotoTaskCompleted(Map<String, Object> payload) {
log.info("STOMP Endpoint '/app/task/photo/completed' called with data: {}", payload);
return processTaskCompletion(payload, "PHOTO");
log.info("STOMP Endpoint '/app/task/photo/completed' called");
return processPhotoTaskCompletion(payload);
}
/**
@@ -360,6 +313,107 @@ public class MessageController {
return processTaskCompletion(payload, "TODOLIST");
}
/**
* Specialized method to process photo task completion with extraData handling.
* Saves photo data to the photos collection and processes task completion.
*/
private Map<String, Object> processPhotoTaskCompletion(Map<String, Object> payload) {
Map<String, Object> response = new java.util.HashMap<>();
response.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
response.put("type", "taskCompletedAck");
if (payload == null || !payload.containsKey("taskId") || payload.get("taskId") == null || payload.get("taskId").toString().isBlank()) {
response.put("success", false);
response.put("message", "taskId ist erforderlich");
log.info("Photo task completion failed: {}", response);
return response;
}
String taskIdStr = payload.get("taskId").toString();
String completedBy = payload.get("completedBy") != null ? payload.get("completedBy").toString() : null;
String note = payload.get("note") != null ? payload.get("note").toString() : null;
try {
org.bson.types.ObjectId taskId = new org.bson.types.ObjectId(taskIdStr);
java.util.Optional<BaseTask> opt = taskRepository.findById(taskId);
if (opt.isEmpty()) {
response.put("success", false);
response.put("message", "Task nicht gefunden");
return response;
}
BaseTask task = opt.get();
// Validate task type is PHOTO
if (!"PHOTO".equals(task.getTaskType())) {
response.put("success", false);
response.put("message", "Task-Typ stimmt nicht mit dem Endpunkt überein. Erwartet: PHOTO, Gefunden: " + task.getTaskType());
log.warn("Task type mismatch for taskId={}: expected=PHOTO, actual={}", taskIdStr, task.getTaskType());
return response;
}
// Process extraData if present
if (payload.containsKey("extraData") && payload.get("extraData") != null) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> extraData = (Map<String, Object>) payload.get("extraData");
if (extraData.containsKey("photos") && extraData.get("photos") != null) {
@SuppressWarnings("unchecked")
List<String> base64Photos = (List<String>) extraData.get("photos");
// Create and save Photo entity
Photo photo = new Photo(task.getJobId(), task.getId(), base64Photos, completedBy);
photoRepository.save(photo);
log.info("Saved {} photos for taskId={}, jobId={}, photoId={}",
base64Photos.size(), taskIdStr, task.getJobIdAsString(), photo.getIdAsString());
response.put("photoId", photo.getIdAsString());
response.put("photosCount", base64Photos.size());
}
} catch (Exception e) {
log.error("Error processing photo extraData for taskId={}: {}", taskIdStr, e.getMessage(), e);
response.put("photoError", "Fehler beim Speichern der Fotos: " + e.getMessage());
}
}
// Complete the task
task.setCompleted(true);
task.setCompletedAt(LocalDateTime.now());
if (completedBy != null) task.setCompletedBy(completedBy);
if (note != null) task.setCompletionNote(note);
taskRepository.save(task);
java.util.Map<String, Object> event = new java.util.HashMap<>();
event.put("taskId", task.getIdAsString());
event.put("jobId", task.getJobIdAsString());
event.put("completed", task.isCompleted());
event.put("completedAt", task.getCompletedAt() != null ? task.getCompletedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null);
event.put("completedBy", task.getCompletedBy());
event.put("note", task.getCompletionNote());
event.put("event", "taskCompleted");
event.put("taskType", task.getTaskType());
// Send specific task topic
messagingTemplate.convertAndSend("/topic/tasks/" + task.getIdAsString(), event);
response.put("success", true);
response.putAll(event);
log.info("Photo task completion processed successfully for taskId={}", taskIdStr);
return response;
} catch (IllegalArgumentException e) {
response.put("success", false);
response.put("message", "Ungültige taskId");
return response;
} catch (Exception e) {
log.error("Error processing photo task completion", e);
response.put("success", false);
response.put("message", "Fehler bei der Verarbeitung");
return response;
}
}
/**
* Common method to process task completion for different task types.
* This method contains the shared logic for all task completion endpoints.

View File

@@ -0,0 +1,258 @@
package de.assecutor.votianlt.controller;
import de.assecutor.votianlt.model.Photo;
import de.assecutor.votianlt.model.task.BaseTask;
import de.assecutor.votianlt.repository.PhotoRepository;
import de.assecutor.votianlt.repository.TaskRepository;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.Base64;
/**
* REST endpoint for uploading photos for PHOTO tasks via HTTP POST instead of STOMP payload.
*
* Provides two content types on the same path:
* - multipart/form-data: files[] (one or many images), optional completedBy, note
* - application/json: { photos: [base64], completedBy?, note? }
*/
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
@Slf4j
public class PhotoUploadController {
@Autowired
private TaskRepository taskRepository;
@Autowired
private PhotoRepository photoRepository;
@Autowired
private SimpMessagingTemplate messagingTemplate;
@PostMapping(path = "/tasks/{taskId}/photos", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Map<String, Object>> uploadPhotosMultipart(
@PathVariable("taskId") String taskId,
@RequestParam(value = "files") List<MultipartFile> files,
@RequestParam(value = "completedBy", required = false) String completedBy,
@RequestParam(value = "note", required = false) String note
) {
Map<String, Object> response = initResponse("photoUploadAck");
if (!StringUtils.hasText(taskId)) {
return badRequest(response, "taskId ist erforderlich");
}
if (files == null || files.isEmpty()) {
return badRequest(response, "Mindestens eine Bilddatei (files) ist erforderlich");
}
try {
ObjectId taskObjectId = new ObjectId(taskId);
Optional<BaseTask> opt = taskRepository.findById(taskObjectId);
if (opt.isEmpty()) {
return notFound(response, "Task nicht gefunden");
}
BaseTask task = opt.get();
if (!"PHOTO".equals(task.getTaskType())) {
return badRequest(response, "Task-Typ stimmt nicht. Erwartet: PHOTO, Gefunden: " + task.getTaskType());
}
// Convert files to base64 strings to keep storage compatible with existing Photo entity
List<String> base64Photos = new ArrayList<>();
for (MultipartFile file : files) {
if (file == null || file.isEmpty()) continue;
String base64 = Base64.getEncoder().encodeToString(file.getBytes());
base64Photos.add(base64);
}
if (base64Photos.isEmpty()) {
return badRequest(response, "Die übermittelten Dateien sind leer");
}
Photo photo = new Photo(task.getJobId(), task.getId(), base64Photos, completedBy);
photoRepository.save(photo);
// Build success response
response.put("success", true);
response.put("photoId", photo.getIdAsString());
response.put("photosCount", base64Photos.size());
response.put("taskId", task.getIdAsString());
response.put("jobId", task.getJobIdAsString());
// Optionally broadcast a small event to task topic
Map<String, Object> event = new HashMap<>();
event.put("event", "photoUploaded");
event.put("taskId", task.getIdAsString());
event.put("jobId", task.getJobIdAsString());
event.put("photosCount", base64Photos.size());
event.put("timestamp", now());
messagingTemplate.convertAndSend("/topic/tasks/" + task.getIdAsString(), event);
log.info("Photo upload (multipart) successful: taskId={}, jobId={}, photoId={}, count={}",
taskId, task.getJobIdAsString(), photo.getIdAsString(), base64Photos.size());
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return badRequest(response, "Ungültige taskId");
} catch (IOException e) {
log.error("Fehler beim Lesen der Dateien: {}", e.getMessage(), e);
return serverError(response, "Fehler beim Lesen der Dateien: " + e.getMessage());
} catch (Exception e) {
log.error("Fehler beim Speichern der Fotos: {}", e.getMessage(), e);
return serverError(response, "Fehler beim Speichern der Fotos");
}
}
@PostMapping(path = "/tasks/{taskId}/photos", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> uploadPhotosJson(
@PathVariable("taskId") String taskId,
@RequestBody Map<String, Object> body
) {
Map<String, Object> response = initResponse("photoUploadAck");
if (!StringUtils.hasText(taskId)) {
return badRequest(response, "taskId ist erforderlich");
}
try {
ObjectId taskObjectId = new ObjectId(taskId);
Optional<BaseTask> opt = taskRepository.findById(taskObjectId);
if (opt.isEmpty()) {
return notFound(response, "Task nicht gefunden");
}
BaseTask task = opt.get();
if (!"PHOTO".equals(task.getTaskType())) {
return badRequest(response, "Task-Typ stimmt nicht. Erwartet: PHOTO, Gefunden: " + task.getTaskType());
}
String completedBy = body.get("completedBy") != null ? body.get("completedBy").toString() : null;
@SuppressWarnings("unchecked")
List<String> photos = body.get("photos") instanceof List ? (List<String>) body.get("photos") : null;
if (photos == null || photos.isEmpty()) {
return badRequest(response, "Feld 'photos' (Liste von Base64-Strings) ist erforderlich");
}
Photo photo = new Photo(task.getJobId(), task.getId(), photos, completedBy);
photoRepository.save(photo);
response.put("success", true);
response.put("photoId", photo.getIdAsString());
response.put("photosCount", photo.getCount());
response.put("taskId", task.getIdAsString());
response.put("jobId", task.getJobIdAsString());
Map<String, Object> event = new HashMap<>();
event.put("event", "photoUploaded");
event.put("taskId", task.getIdAsString());
event.put("jobId", task.getJobIdAsString());
event.put("photosCount", photo.getCount());
event.put("timestamp", now());
messagingTemplate.convertAndSend("/topic/tasks/" + task.getIdAsString(), event);
log.info("Photo upload (json) successful: taskId={}, jobId={}, photoId={}, count={}",
taskId, task.getJobIdAsString(), photo.getIdAsString(), photo.getCount());
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return badRequest(response, "Ungültige taskId");
} catch (Exception e) {
log.error("Fehler beim Speichern der Fotos: {}", e.getMessage(), e);
return serverError(response, "Fehler beim Speichern der Fotos");
}
}
/**
* New simple JSON endpoint: accept a single base64 photo with taskId in the body.
* If there are multiple photos, call this endpoint multiple times.
* Body: { "taskId": "<ObjectId>", "photo": "<base64>", "completedBy"?: "...", "note"?: "..." }
*/
@PostMapping(path = "/photos", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> uploadSinglePhotoJson(
@RequestBody Map<String, Object> body
) {
Map<String, Object> response = initResponse("photoUploadAck");
String taskId = body != null && body.get("taskId") != null ? body.get("taskId").toString() : null;
String completedBy = body != null && body.get("completedBy") != null ? body.get("completedBy").toString() : null;
String base64Photo = body != null && body.get("photo") != null ? body.get("photo").toString() : null;
if (!StringUtils.hasText(taskId)) {
return badRequest(response, "taskId ist erforderlich");
}
if (!StringUtils.hasText(base64Photo)) {
return badRequest(response, "Feld 'photo' (Base64-String) ist erforderlich");
}
try {
ObjectId taskObjectId = new ObjectId(taskId);
Optional<BaseTask> opt = taskRepository.findById(taskObjectId);
if (opt.isEmpty()) {
return notFound(response, "Task nicht gefunden");
}
BaseTask task = opt.get();
if (!"PHOTO".equals(task.getTaskType())) {
return badRequest(response, "Task-Typ stimmt nicht. Erwartet: PHOTO, Gefunden: " + task.getTaskType());
}
Photo photo = new Photo(task.getJobId(), task.getId(), java.util.List.of(base64Photo), completedBy);
photoRepository.save(photo);
response.put("success", true);
response.put("photoId", photo.getIdAsString());
response.put("photosCount", 1);
response.put("taskId", task.getIdAsString());
response.put("jobId", task.getJobIdAsString());
Map<String, Object> event = new HashMap<>();
event.put("event", "photoUploaded");
event.put("taskId", task.getIdAsString());
event.put("jobId", task.getJobIdAsString());
event.put("photosCount", 1);
event.put("timestamp", now());
messagingTemplate.convertAndSend("/topic/tasks/" + task.getIdAsString(), event);
log.info("Photo upload (single json) successful: taskId={}, jobId={}, photoId={}",
taskId, task.getJobIdAsString(), photo.getIdAsString());
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return badRequest(response, "Ungültige taskId");
} catch (Exception e) {
log.error("Fehler beim Speichern des Fotos: {}", e.getMessage(), e);
return serverError(response, "Fehler beim Speichern des Fotos");
}
}
private Map<String, Object> initResponse(String type) {
Map<String, Object> map = new HashMap<>();
map.put("timestamp", now());
map.put("type", type);
return map;
}
private String now() {
return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
private ResponseEntity<Map<String, Object>> badRequest(Map<String, Object> response, String msg) {
response.put("success", false);
response.put("message", msg);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
private ResponseEntity<Map<String, Object>> notFound(Map<String, Object> response, String msg) {
response.put("success", false);
response.put("message", msg);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
private ResponseEntity<Map<String, Object>> serverError(Map<String, Object> response, String msg) {
response.put("success", false);
response.put("message", msg);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}

View File

@@ -0,0 +1,123 @@
package de.assecutor.votianlt.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.bson.types.ObjectId;
import java.time.LocalDateTime;
import java.util.List;
/**
* Photo entity for storing photo data from task completions.
* References the job ObjectId and stores base64 encoded photos.
*/
@Document(collection = "photos")
public class Photo {
@Id
private ObjectId id;
private ObjectId jobId;
private ObjectId taskId;
private List<String> photos; // base64 encoded photos
private int count;
private LocalDateTime createdAt;
private String completedBy;
// Default constructor
public Photo() {
this.createdAt = LocalDateTime.now();
}
// Constructor with parameters
public Photo(ObjectId jobId, ObjectId taskId, List<String> photos, String completedBy) {
this();
this.jobId = jobId;
this.taskId = taskId;
this.photos = photos;
this.count = photos != null ? photos.size() : 0;
this.completedBy = completedBy;
}
// Getters and Setters
public ObjectId getId() {
return id;
}
public void setId(ObjectId id) {
this.id = id;
}
public String getIdAsString() {
return id != null ? id.toHexString() : null;
}
public ObjectId getJobId() {
return jobId;
}
public void setJobId(ObjectId jobId) {
this.jobId = jobId;
}
public String getJobIdAsString() {
return jobId != null ? jobId.toHexString() : null;
}
public ObjectId getTaskId() {
return taskId;
}
public void setTaskId(ObjectId taskId) {
this.taskId = taskId;
}
public String getTaskIdAsString() {
return taskId != null ? taskId.toHexString() : null;
}
public List<String> getPhotos() {
return photos;
}
public void setPhotos(List<String> photos) {
this.photos = photos;
this.count = photos != null ? photos.size() : 0;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getCompletedBy() {
return completedBy;
}
public void setCompletedBy(String completedBy) {
this.completedBy = completedBy;
}
@Override
public String toString() {
return "Photo{" +
"id=" + id +
", jobId=" + jobId +
", taskId=" + taskId +
", count=" + count +
", createdAt=" + createdAt +
", completedBy='" + completedBy + '\'' +
'}';
}
}

View File

@@ -0,0 +1,48 @@
package de.assecutor.votianlt.repository;
import de.assecutor.votianlt.model.Photo;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* Repository interface for Photo entities.
* Provides database operations for the photos collection.
*/
@Repository
public interface PhotoRepository extends MongoRepository<Photo, ObjectId> {
/**
* Find all photos associated with a specific job ID.
* @param jobId The ObjectId of the job
* @return List of photos for the job
*/
List<Photo> findByJobId(ObjectId jobId);
/**
* Find all photos associated with a specific task ID.
* @param taskId The ObjectId of the task
* @return List of photos for the task
*/
List<Photo> findByTaskId(ObjectId taskId);
/**
* Find photos by job ID as string.
* @param jobId The job ID as string
* @return List of photos for the job
*/
default List<Photo> findByJobId(String jobId) {
return findByJobId(new ObjectId(jobId));
}
/**
* Find photos by task ID as string.
* @param taskId The task ID as string
* @return List of photos for the task
*/
default List<Photo> findByTaskId(String taskId) {
return findByTaskId(new ObjectId(taskId));
}
}

View File

@@ -29,14 +29,26 @@ mail.smtp.host=smtp.ionos.de
mail.smtp.port=587
# WebSocket and STOMP Configuration
# WebSocket message size limits (in bytes)
spring.websocket.servlet.max-text-message-buffer-size=8192
spring.websocket.servlet.max-binary-message-buffer-size=8192
# WebSocket message size limits (in bytes) - Increased for large photo payloads
# Enable STOMP over WebSocket
spring.websocket.stomp.enabled=true
# STOMP heartbeat settings (in milliseconds)
spring.websocket.stomp.heartbeat.outgoing=10000
spring.websocket.stomp.heartbeat.incoming=10000
# HTTP request size limits for large payloads
server.max-http-request-header-size=8MB
# Spring messaging size limits for STOMP
# Tomcat connector limits
server.tomcat.max-http-form-post-size=64MB
server.tomcat.max-save-post-size=64MB
server.tomcat.max-swallow-size=64MB
# Multipart upload limits for photo HTTP uploads
spring.servlet.multipart.max-file-size=32MB
spring.servlet.multipart.max-request-size=64MB
# Additional WebSocket and messaging limits for large payloads
# STOMP broker relay limits
# Jackson message converter limits
spring.jackson.default-property-inclusion=non_null
# 2FA Configuration
app.security.two-factor.enabled=false