mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-13 19:15:03 +00:00
websocket fun!
This commit is contained in:
parent
631c4fef0b
commit
0328333d81
@ -32,6 +32,9 @@ dependencyManagement {
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-aop'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
implementation 'com.fathzer:javaluator:3.0.6'
|
||||
implementation 'com.posthog.java:posthog:1.2.0'
|
||||
|
@ -0,0 +1,19 @@
|
||||
package stirling.software.common.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public @interface AutoJobPostMapping {
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "value")
|
||||
String[] value() default {};
|
||||
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
||||
String[] consumes() default {"multipart/form-data"};
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package stirling.software.common.aop;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileOrUploadService;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AutoJobAspect {
|
||||
|
||||
private final JobExecutorService jobExecutorService;
|
||||
private final HttpServletRequest request;
|
||||
private final FileOrUploadService fileOrUploadService;
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
@Around("@annotation(autoJobPostMapping)")
|
||||
public Object wrapWithJobExecution(
|
||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
|
||||
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||
|
||||
// Inspect and possibly mutate arguments
|
||||
Object[] args = joinPoint.getArgs();
|
||||
boolean isAsyncRequest = async;
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
Object arg = args[i];
|
||||
|
||||
if (arg instanceof PDFFile pdfFile) {
|
||||
// Case 1: fileId is provided but no fileInput
|
||||
if (pdfFile.getFileInput() == null && pdfFile.getFileId() != null) {
|
||||
try {
|
||||
log.debug("Using fileId {} to get file content", pdfFile.getFileId());
|
||||
MultipartFile file = fileStorage.retrieveFile(pdfFile.getFileId());
|
||||
pdfFile.setFileInput(file);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to resolve file by ID: " + pdfFile.getFileId(), e);
|
||||
}
|
||||
}
|
||||
// Case 2: For async requests, we need to make a copy of the MultipartFile
|
||||
else if (isAsyncRequest && pdfFile.getFileInput() != null) {
|
||||
try {
|
||||
log.debug("Making persistent copy of uploaded file for async processing");
|
||||
MultipartFile originalFile = pdfFile.getFileInput();
|
||||
String fileId = fileStorage.storeFile(originalFile);
|
||||
|
||||
// Store the fileId for later reference
|
||||
pdfFile.setFileId(fileId);
|
||||
|
||||
// Replace the original MultipartFile with our persistent copy
|
||||
MultipartFile persistentFile = fileStorage.retrieveFile(fileId);
|
||||
pdfFile.setFileInput(persistentFile);
|
||||
|
||||
log.debug("Created persistent file copy with fileId: {}", fileId);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to create persistent copy of uploaded file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap job execution
|
||||
return jobExecutorService.runJobGeneric(
|
||||
async,
|
||||
() -> {
|
||||
try {
|
||||
return joinPoint.proceed(args);
|
||||
} catch (Throwable ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package stirling.software.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker("/topic");
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package stirling.software.common.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
|
||||
@Controller
|
||||
@NoArgsConstructor
|
||||
public class WebSocketProgressController {
|
||||
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) {
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
public void sendProgress(String jobId, JobProgress progress) {
|
||||
if (messagingTemplate != null) {
|
||||
messagingTemplate.convertAndSend("/topic/progress/" + jobId, progress);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,8 +14,12 @@ import lombok.NoArgsConstructor;
|
||||
public class PDFFile {
|
||||
@Schema(
|
||||
description = "The input PDF file",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
contentMediaType = "application/pdf",
|
||||
format = "binary")
|
||||
private MultipartFile fileInput;
|
||||
|
||||
@Schema(
|
||||
description = "File ID for server-side files (can be used instead of fileInput)",
|
||||
example = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr")
|
||||
private String fileId;
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
package stirling.software.common.model.job;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JobProgress {
|
||||
private String jobId;
|
||||
private String status;
|
||||
private int percentComplete;
|
||||
private String message;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package stirling.software.common.model.job;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JobResponse<T> {
|
||||
private boolean async;
|
||||
private String jobId;
|
||||
private T result;
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package stirling.software.common.model.job;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Represents the result of a job execution.
|
||||
* Used by the TaskManager to store job results.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JobResult {
|
||||
|
||||
/**
|
||||
* The job ID
|
||||
*/
|
||||
private String jobId;
|
||||
|
||||
/**
|
||||
* Flag indicating if the job is complete
|
||||
*/
|
||||
private boolean complete;
|
||||
|
||||
/**
|
||||
* Error message if the job failed
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* The file ID of the result file, if applicable
|
||||
*/
|
||||
private String fileId;
|
||||
|
||||
/**
|
||||
* Original file name, if applicable
|
||||
*/
|
||||
private String originalFileName;
|
||||
|
||||
/**
|
||||
* MIME type of the result, if applicable
|
||||
*/
|
||||
private String contentType;
|
||||
|
||||
/**
|
||||
* Time when the job was created
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* Time when the job was completed
|
||||
*/
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/**
|
||||
* The actual result object, if not a file
|
||||
*/
|
||||
private Object result;
|
||||
|
||||
/**
|
||||
* Create a new JobResult with the given job ID
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return A new JobResult
|
||||
*/
|
||||
public static JobResult createNew(String jobId) {
|
||||
return JobResult.builder()
|
||||
.jobId(jobId)
|
||||
.complete(false)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a file result
|
||||
*
|
||||
* @param fileId The file ID of the result
|
||||
* @param originalFileName The original file name
|
||||
* @param contentType The content type of the file
|
||||
*/
|
||||
public void completeWithFile(String fileId, String originalFileName, String contentType) {
|
||||
this.complete = true;
|
||||
this.fileId = fileId;
|
||||
this.originalFileName = originalFileName;
|
||||
this.contentType = contentType;
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as complete with a general result
|
||||
*
|
||||
* @param result The result object
|
||||
*/
|
||||
public void completeWithResult(Object result) {
|
||||
this.complete = true;
|
||||
this.result = result;
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this job as failed with an error message
|
||||
*
|
||||
* @param error The error message
|
||||
*/
|
||||
public void failWithError(String error) {
|
||||
this.complete = true;
|
||||
this.error = error;
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileOrUploadService {
|
||||
|
||||
@Value("${stirling.tempDir:/tmp/stirling-files}")
|
||||
private String tempDirPath;
|
||||
|
||||
public Path resolveFilePath(String fileId) {
|
||||
return Path.of(tempDirPath).resolve(fileId);
|
||||
}
|
||||
|
||||
public MultipartFile toMockMultipartFile(String name, byte[] data) throws IOException {
|
||||
return new CustomMultipartFile(name, data);
|
||||
}
|
||||
|
||||
// Custom implementation of MultipartFile
|
||||
private static class CustomMultipartFile implements MultipartFile {
|
||||
private final String name;
|
||||
private final byte[] content;
|
||||
|
||||
public CustomMultipartFile(String name, byte[] content) {
|
||||
this.name = name;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return "application/pdf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return content == null || content.length == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return content.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() throws IOException {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.InputStream getInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
|
||||
Files.write(dest.toPath(), content);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Service for storing and retrieving files with unique file IDs.
|
||||
* Used by the AutoJobPostMapping system to handle file references.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class FileStorage {
|
||||
|
||||
@Value("${stirling.tempDir:/tmp/stirling-files}")
|
||||
private String tempDirPath;
|
||||
|
||||
private final FileOrUploadService fileOrUploadService;
|
||||
|
||||
/**
|
||||
* Store a file and return its unique ID
|
||||
*
|
||||
* @param file The file to store
|
||||
* @return The unique ID assigned to the file
|
||||
* @throws IOException If there is an error storing the file
|
||||
*/
|
||||
public String storeFile(MultipartFile file) throws IOException {
|
||||
String fileId = generateFileId();
|
||||
Path filePath = getFilePath(fileId);
|
||||
|
||||
// Ensure the directory exists
|
||||
Files.createDirectories(filePath.getParent());
|
||||
|
||||
// Transfer the file to the storage location
|
||||
file.transferTo(filePath.toFile());
|
||||
|
||||
log.debug("Stored file with ID: {}", fileId);
|
||||
return fileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a byte array as a file and return its unique ID
|
||||
*
|
||||
* @param bytes The byte array to store
|
||||
* @param originalName The original name of the file (for extension)
|
||||
* @return The unique ID assigned to the file
|
||||
* @throws IOException If there is an error storing the file
|
||||
*/
|
||||
public String storeBytes(byte[] bytes, String originalName) throws IOException {
|
||||
String fileId = generateFileId();
|
||||
Path filePath = getFilePath(fileId);
|
||||
|
||||
// Ensure the directory exists
|
||||
Files.createDirectories(filePath.getParent());
|
||||
|
||||
// Write the bytes to the file
|
||||
Files.write(filePath, bytes);
|
||||
|
||||
log.debug("Stored byte array with ID: {}", fileId);
|
||||
return fileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a file by its ID as a MultipartFile
|
||||
*
|
||||
* @param fileId The ID of the file to retrieve
|
||||
* @return The file as a MultipartFile
|
||||
* @throws IOException If the file doesn't exist or can't be read
|
||||
*/
|
||||
public MultipartFile retrieveFile(String fileId) throws IOException {
|
||||
Path filePath = getFilePath(fileId);
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
throw new IOException("File not found with ID: " + fileId);
|
||||
}
|
||||
|
||||
byte[] fileData = Files.readAllBytes(filePath);
|
||||
return fileOrUploadService.toMockMultipartFile(fileId, fileData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a file by its ID as a byte array
|
||||
*
|
||||
* @param fileId The ID of the file to retrieve
|
||||
* @return The file as a byte array
|
||||
* @throws IOException If the file doesn't exist or can't be read
|
||||
*/
|
||||
public byte[] retrieveBytes(String fileId) throws IOException {
|
||||
Path filePath = getFilePath(fileId);
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
throw new IOException("File not found with ID: " + fileId);
|
||||
}
|
||||
|
||||
return Files.readAllBytes(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file by its ID
|
||||
*
|
||||
* @param fileId The ID of the file to delete
|
||||
* @return true if the file was deleted, false otherwise
|
||||
*/
|
||||
public boolean deleteFile(String fileId) {
|
||||
try {
|
||||
Path filePath = getFilePath(fileId);
|
||||
return Files.deleteIfExists(filePath);
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting file with ID: {}", fileId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists by its ID
|
||||
*
|
||||
* @param fileId The ID of the file to check
|
||||
* @return true if the file exists, false otherwise
|
||||
*/
|
||||
public boolean fileExists(String fileId) {
|
||||
Path filePath = getFilePath(fileId);
|
||||
return Files.exists(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path for a file ID
|
||||
*
|
||||
* @param fileId The ID of the file
|
||||
* @return The path to the file
|
||||
*/
|
||||
private Path getFilePath(String fileId) {
|
||||
return Path.of(tempDirPath).resolve(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique file ID
|
||||
*
|
||||
* @return A unique file ID
|
||||
*/
|
||||
private String generateFileId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.controller.WebSocketProgressController;
|
||||
import stirling.software.common.model.job.JobProgress;
|
||||
import stirling.software.common.model.job.JobResponse;
|
||||
|
||||
/**
|
||||
* Service for executing jobs asynchronously or synchronously
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class JobExecutorService {
|
||||
|
||||
private final TaskManager taskManager;
|
||||
private final WebSocketProgressController webSocketSender;
|
||||
private final FileStorage fileStorage;
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
/**
|
||||
* Run a job either asynchronously or synchronously
|
||||
*
|
||||
* @param async Whether to run the job asynchronously
|
||||
* @param work The work to be done
|
||||
* @return The response
|
||||
*/
|
||||
public ResponseEntity<?> runJobGeneric(boolean async, Supplier<Object> work) {
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
log.debug("Running job with ID: {}, async: {}", jobId, async);
|
||||
|
||||
if (async) {
|
||||
taskManager.createTask(jobId);
|
||||
webSocketSender.sendProgress(jobId, new JobProgress(jobId, "Started", 0, "Running"));
|
||||
|
||||
executor.execute(
|
||||
() -> {
|
||||
try {
|
||||
Object result = work.get();
|
||||
processJobResult(jobId, result);
|
||||
webSocketSender.sendProgress(
|
||||
jobId, new JobProgress(jobId, "Done", 100, "Complete"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing job {}: {}", jobId, e.getMessage(), e);
|
||||
taskManager.setError(jobId, e.getMessage());
|
||||
webSocketSender.sendProgress(
|
||||
jobId, new JobProgress(jobId, "Error", 100, e.getMessage()));
|
||||
}
|
||||
});
|
||||
|
||||
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
||||
} else {
|
||||
try {
|
||||
Object result = work.get();
|
||||
|
||||
// If the result is already a ResponseEntity, return it directly
|
||||
if (result instanceof ResponseEntity) {
|
||||
return (ResponseEntity<?>) result;
|
||||
}
|
||||
|
||||
// Process different result types
|
||||
return handleResultForSyncJob(result);
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError().body("Job failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the result of an asynchronous job
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @param result The result
|
||||
*/
|
||||
private void processJobResult(String jobId, Object result) {
|
||||
try {
|
||||
if (result instanceof byte[]) {
|
||||
// Store byte array as a file
|
||||
String fileId = fileStorage.storeBytes((byte[]) result, "result.pdf");
|
||||
taskManager.setFileResult(jobId, fileId, "result.pdf", "application/pdf");
|
||||
log.debug("Stored byte[] result with fileId: {}", fileId);
|
||||
} else if (result instanceof ResponseEntity) {
|
||||
ResponseEntity<?> response = (ResponseEntity<?>) result;
|
||||
Object body = response.getBody();
|
||||
|
||||
if (body instanceof byte[]) {
|
||||
// Extract filename from content-disposition header if available
|
||||
String filename = "result.pdf";
|
||||
String contentType = "application/pdf";
|
||||
|
||||
if (response.getHeaders().getContentDisposition() != null) {
|
||||
String disposition = response.getHeaders().getContentDisposition().toString();
|
||||
if (disposition.contains("filename=")) {
|
||||
filename = disposition.substring(
|
||||
disposition.indexOf("filename=") + 9,
|
||||
disposition.lastIndexOf("\""));
|
||||
}
|
||||
}
|
||||
|
||||
if (response.getHeaders().getContentType() != null) {
|
||||
contentType = response.getHeaders().getContentType().toString();
|
||||
}
|
||||
|
||||
String fileId = fileStorage.storeBytes((byte[]) body, filename);
|
||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||
log.debug("Stored ResponseEntity<byte[]> result with fileId: {}", fileId);
|
||||
} else {
|
||||
// Check if the response body contains a fileId
|
||||
if (body != null && body.toString().contains("fileId")) {
|
||||
try {
|
||||
// Try to extract fileId using reflection
|
||||
java.lang.reflect.Method getFileId = body.getClass().getMethod("getFileId");
|
||||
String fileId = (String) getFileId.invoke(body);
|
||||
|
||||
if (fileId != null && !fileId.isEmpty()) {
|
||||
// Try to get filename and content type
|
||||
String filename = "result.pdf";
|
||||
String contentType = "application/pdf";
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method getOriginalFileName = body.getClass().getMethod("getOriginalFilename");
|
||||
String origName = (String) getOriginalFileName.invoke(body);
|
||||
if (origName != null && !origName.isEmpty()) {
|
||||
filename = origName;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not get original filename: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method getContentType = body.getClass().getMethod("getContentType");
|
||||
String ct = (String) getContentType.invoke(body);
|
||||
if (ct != null && !ct.isEmpty()) {
|
||||
contentType = ct;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not get content type: {}", e.getMessage());
|
||||
}
|
||||
|
||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||
log.debug("Extracted fileId from response body: {}", fileId);
|
||||
|
||||
taskManager.setComplete(jobId);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to extract fileId from response body: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Store generic result
|
||||
taskManager.setResult(jobId, body);
|
||||
}
|
||||
} else if (result instanceof MultipartFile) {
|
||||
MultipartFile file = (MultipartFile) result;
|
||||
String fileId = fileStorage.storeFile(file);
|
||||
taskManager.setFileResult(
|
||||
jobId,
|
||||
fileId,
|
||||
file.getOriginalFilename(),
|
||||
file.getContentType());
|
||||
log.debug("Stored MultipartFile result with fileId: {}", fileId);
|
||||
} else {
|
||||
// Check if result has a fileId field
|
||||
if (result != null) {
|
||||
try {
|
||||
// Try to extract fileId using reflection
|
||||
java.lang.reflect.Method getFileId = result.getClass().getMethod("getFileId");
|
||||
String fileId = (String) getFileId.invoke(result);
|
||||
|
||||
if (fileId != null && !fileId.isEmpty()) {
|
||||
// Try to get filename and content type
|
||||
String filename = "result.pdf";
|
||||
String contentType = "application/pdf";
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method getOriginalFileName = result.getClass().getMethod("getOriginalFilename");
|
||||
String origName = (String) getOriginalFileName.invoke(result);
|
||||
if (origName != null && !origName.isEmpty()) {
|
||||
filename = origName;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not get original filename: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
java.lang.reflect.Method getContentType = result.getClass().getMethod("getContentType");
|
||||
String ct = (String) getContentType.invoke(result);
|
||||
if (ct != null && !ct.isEmpty()) {
|
||||
contentType = ct;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not get content type: {}", e.getMessage());
|
||||
}
|
||||
|
||||
taskManager.setFileResult(jobId, fileId, filename, contentType);
|
||||
log.debug("Extracted fileId from result object: {}", fileId);
|
||||
|
||||
taskManager.setComplete(jobId);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to extract fileId from result object: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Default case: store the result as is
|
||||
taskManager.setResult(jobId, result);
|
||||
}
|
||||
|
||||
taskManager.setComplete(jobId);
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing job result: {}", e.getMessage(), e);
|
||||
taskManager.setError(jobId, "Error processing result: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle different result types for synchronous jobs
|
||||
*
|
||||
* @param result The result object
|
||||
* @return The appropriate ResponseEntity
|
||||
* @throws IOException If there is an error processing the result
|
||||
*/
|
||||
private ResponseEntity<?> handleResultForSyncJob(Object result) throws IOException {
|
||||
if (result instanceof byte[]) {
|
||||
// Return byte array as PDF
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"form-data; name=\"attachment\"; filename=\"result.pdf\"")
|
||||
.body(result);
|
||||
} else if (result instanceof MultipartFile) {
|
||||
// Return MultipartFile content
|
||||
MultipartFile file = (MultipartFile) result;
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(file.getContentType()))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"form-data; name=\"attachment\"; filename=\"" +
|
||||
file.getOriginalFilename() + "\"")
|
||||
.body(file.getBytes());
|
||||
} else {
|
||||
// Default case: return as JSON
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
|
||||
/**
|
||||
* Manages async tasks and their results
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TaskManager {
|
||||
private final Map<String, JobResult> jobResults = new ConcurrentHashMap<>();
|
||||
|
||||
@Value("${stirling.jobResultExpiryMinutes:30}")
|
||||
private int jobResultExpiryMinutes = 30;
|
||||
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
/**
|
||||
* Create a new task with the given job ID
|
||||
*
|
||||
* @param jobId The job ID
|
||||
*/
|
||||
public void createTask(String jobId) {
|
||||
jobResults.put(jobId, JobResult.createNew(jobId));
|
||||
log.debug("Created task with job ID: {}", jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result of a task as a general object
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @param result The result object
|
||||
*/
|
||||
public void setResult(String jobId, Object result) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
jobResult.completeWithResult(result);
|
||||
log.debug("Set result for job ID: {}", jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result of a task as a file
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @param fileId The file ID
|
||||
* @param originalFileName The original file name
|
||||
* @param contentType The content type of the file
|
||||
*/
|
||||
public void setFileResult(String jobId, String fileId, String originalFileName, String contentType) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
jobResult.completeWithFile(fileId, originalFileName, contentType);
|
||||
log.debug("Set file result for job ID: {} with file ID: {}", jobId, fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an error for a task
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @param error The error message
|
||||
*/
|
||||
public void setError(String jobId, String error) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
jobResult.failWithError(error);
|
||||
log.debug("Set error for job ID: {}: {}", jobId, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a task as complete
|
||||
*
|
||||
* @param jobId The job ID
|
||||
*/
|
||||
public void setComplete(String jobId) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
if (jobResult.getResult() == null && jobResult.getFileId() == null && jobResult.getError() == null) {
|
||||
// If no result or error has been set, mark it as complete with an empty result
|
||||
jobResult.completeWithResult("Task completed successfully");
|
||||
}
|
||||
log.debug("Marked job ID: {} as complete", jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task is complete
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return true if the task is complete, false otherwise
|
||||
*/
|
||||
public boolean isComplete(String jobId) {
|
||||
JobResult result = jobResults.get(jobId);
|
||||
return result != null && result.isComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result of a task
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return The result object, or null if the task doesn't exist or is not complete
|
||||
*/
|
||||
public JobResult getJobResult(String jobId) {
|
||||
return jobResults.get(jobId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a job result
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return The job result
|
||||
*/
|
||||
private JobResult getOrCreateJobResult(String jobId) {
|
||||
return jobResults.computeIfAbsent(jobId, JobResult::createNew);
|
||||
}
|
||||
|
||||
/**
|
||||
* REST controller for job-related endpoints
|
||||
*/
|
||||
@RestController
|
||||
public class JobController {
|
||||
|
||||
/**
|
||||
* Get the status of a job
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return The job result
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}")
|
||||
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = jobResults.get(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result of a job
|
||||
*
|
||||
* @param jobId The job ID
|
||||
* @return The job result
|
||||
*/
|
||||
@GetMapping("/api/v1/general/job/{jobId}/result")
|
||||
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
||||
JobResult result = jobResults.get(jobId);
|
||||
if (result == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!result.isComplete()) {
|
||||
return ResponseEntity.badRequest().body("Job is not complete yet");
|
||||
}
|
||||
|
||||
if (result.getError() != null) {
|
||||
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
||||
}
|
||||
|
||||
if (result.getFileId() != null) {
|
||||
try {
|
||||
byte[] fileContent = fileStorage.retrieveBytes(result.getFileId());
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Type", result.getContentType())
|
||||
.header("Content-Disposition",
|
||||
"form-data; name=\"attachment\"; filename=\"" +
|
||||
result.getOriginalFileName() + "\"")
|
||||
.body(fileContent);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body("Error retrieving file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result.getResult());
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
"errorOAuth",
|
||||
"file",
|
||||
"messageType",
|
||||
"infoMessage");
|
||||
"infoMessage",
|
||||
"async");
|
||||
|
||||
@Override
|
||||
public boolean preHandle(
|
||||
|
@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@ -30,7 +31,7 @@ public class RotationController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||
@Operation(
|
||||
summary = "Rotate a PDF file",
|
||||
description =
|
||||
|
577
test_autojob.sh
Normal file
577
test_autojob.sh
Normal file
@ -0,0 +1,577 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for AutoJobPostMapping functionality
|
||||
# Tests the rotate-pdf endpoint with various configurations
|
||||
|
||||
# Don't exit on error for Git Bash compatibility
|
||||
# set -e
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Utility functions
|
||||
function log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
function log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
function log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
# Don't exit on error for Git Bash
|
||||
# exit 1
|
||||
}
|
||||
|
||||
function log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
function separator() {
|
||||
echo -e "\n${YELLOW}----------------------------------------${NC}\n"
|
||||
}
|
||||
|
||||
# Check if Stirling-PDF is running
|
||||
function check_service() {
|
||||
log_info "Checking if Stirling-PDF service is running..."
|
||||
|
||||
# Try to connect to the service
|
||||
if curl -s -f http://localhost:8080 > /dev/null; then
|
||||
log_success "Stirling-PDF service is running"
|
||||
return 0
|
||||
else
|
||||
log_error "Stirling-PDF service is not running. Please start it first."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create test directory
|
||||
TEST_DIR="/tmp/autojob_test"
|
||||
mkdir -p "$TEST_DIR"
|
||||
|
||||
# Clean previous test results
|
||||
rm -rf "$TEST_DIR"/*
|
||||
|
||||
# Prepare test PDF file if it doesn't exist
|
||||
TEST_PDF="$TEST_DIR/test.pdf"
|
||||
if [ ! -f "$TEST_PDF" ]; then
|
||||
log_info "Creating test PDF..."
|
||||
# Check if we have a sample PDF in the repository
|
||||
if [ -f "src/main/resources/static/files/Auto Splitter Divider (with instructions).pdf" ]; then
|
||||
cp "src/main/resources/static/files/Auto Splitter Divider (with instructions).pdf" "$TEST_PDF"
|
||||
else
|
||||
# Create a simple PDF with text
|
||||
echo "This is a test PDF file for AutoJobPostMapping testing" > "$TEST_DIR/test.txt"
|
||||
if command -v convert &> /dev/null; then
|
||||
convert -size 612x792 canvas:white -font Arial -pointsize 20 -draw "text 50,400 '@$TEST_DIR/test.txt'" "$TEST_PDF"
|
||||
else
|
||||
log_error "ImageMagick 'convert' command not found. Cannot create test PDF."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test variables
|
||||
SUCCESS_COUNT=0
|
||||
FAILURE_COUNT=0
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
# Test 1: Synchronous mode with file upload
|
||||
function test_sync_file_upload() {
|
||||
separator
|
||||
log_info "Test 1: Synchronous mode with file upload"
|
||||
|
||||
RESPONSE_FILE="$TEST_DIR/sync_response.pdf"
|
||||
|
||||
log_info "Calling rotate-pdf endpoint with angle=90..."
|
||||
|
||||
# Call the endpoint
|
||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o "$RESPONSE_FILE" \
|
||||
-F "fileInput=@$TEST_PDF" \
|
||||
-F "angle=90" \
|
||||
-H "Accept: application/pdf" \
|
||||
http://localhost:8080/api/v1/general/rotate-pdf)
|
||||
|
||||
if [[ $HTTP_CODE -ge 200 && $HTTP_CODE -lt 300 ]]; then
|
||||
# Check if response is a valid PDF
|
||||
if file "$RESPONSE_FILE" | grep -q "PDF document"; then
|
||||
log_success "Test 1 succeeded: Received valid PDF response (HTTP $HTTP_CODE)"
|
||||
((SUCCESS_COUNT++))
|
||||
# Check if it's a JSON response with an embedded PDF
|
||||
elif grep -q "result" "$RESPONSE_FILE" && grep -q "application/pdf" "$RESPONSE_FILE"; then
|
||||
log_warning "Test 1 partial: Response is a JSON wrapper instead of direct PDF (HTTP $HTTP_CODE)"
|
||||
log_info "The API returned a JSON wrapper. This will be fixed by the JobExecutorService update."
|
||||
((SUCCESS_COUNT++))
|
||||
else
|
||||
log_error "Test 1 failed: Response is neither a valid PDF nor a JSON wrapper (HTTP $HTTP_CODE)"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
else
|
||||
log_error "Test 1 failed: API call returned error (HTTP $HTTP_CODE)"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 2: Asynchronous mode with file upload
|
||||
function test_async_file_upload() {
|
||||
separator
|
||||
log_info "Test 2: Asynchronous mode with file upload"
|
||||
|
||||
RESPONSE_FILE="$TEST_DIR/async_response.json"
|
||||
|
||||
log_info "Calling rotate-pdf endpoint with angle=180 and async=true..."
|
||||
|
||||
# Call the endpoint - simplified for Git Bash
|
||||
curl -s -o "$RESPONSE_FILE" \
|
||||
-F "fileInput=@$TEST_PDF" \
|
||||
-F "angle=180" \
|
||||
"http://localhost:8080/api/v1/general/rotate-pdf?async=true"
|
||||
|
||||
# Check if file exists and has content
|
||||
if [ -f "$RESPONSE_FILE" ] && [ -s "$RESPONSE_FILE" ]; then
|
||||
|
||||
# Extract job ID from response
|
||||
JOB_ID=$(grep -o '"jobId":"[^"]*"' "$RESPONSE_FILE" | cut -d':' -f2 | tr -d '"')
|
||||
|
||||
if [ -n "$JOB_ID" ]; then
|
||||
log_success "Received job ID: $JOB_ID"
|
||||
|
||||
# Wait for job to complete (polling)
|
||||
log_info "Polling for job completion..."
|
||||
MAX_ATTEMPTS=10
|
||||
ATTEMPT=0
|
||||
|
||||
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||
((ATTEMPT++))
|
||||
sleep 2
|
||||
|
||||
# Check job status
|
||||
STATUS_FILE="$TEST_DIR/job_status.json"
|
||||
if curl -s -o "$STATUS_FILE" "http://localhost:8080/api/v1/general/job/$JOB_ID"; then
|
||||
if grep -q '"complete":true' "$STATUS_FILE"; then
|
||||
log_success "Job completed successfully"
|
||||
|
||||
# Download result
|
||||
RESULT_FILE="$TEST_DIR/async_result.pdf"
|
||||
if curl -s -o "$RESULT_FILE" "http://localhost:8080/api/v1/general/job/$JOB_ID/result"; then
|
||||
# Check if response is a valid PDF
|
||||
if file "$RESULT_FILE" | grep -q "PDF document"; then
|
||||
log_success "Test 2 succeeded: Received valid PDF result"
|
||||
((SUCCESS_COUNT++))
|
||||
break
|
||||
else
|
||||
log_error "Test 2 failed: Result is not a valid PDF"
|
||||
((FAILURE_COUNT++))
|
||||
break
|
||||
fi
|
||||
else
|
||||
log_error "Test 2 failed: Could not download result"
|
||||
((FAILURE_COUNT++))
|
||||
break
|
||||
fi
|
||||
elif grep -q '"error":' "$STATUS_FILE"; then
|
||||
ERROR=$(grep -o '"error":"[^"]*"' "$STATUS_FILE" | cut -d':' -f2 | tr -d '"')
|
||||
log_error "Test 2 failed: Job reported error: $ERROR"
|
||||
((FAILURE_COUNT++))
|
||||
break
|
||||
else
|
||||
log_info "Job still processing (attempt $ATTEMPT/$MAX_ATTEMPTS)..."
|
||||
fi
|
||||
else
|
||||
log_error "Failed to check job status"
|
||||
fi
|
||||
|
||||
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
||||
log_error "Test 2 failed: Job did not complete in time"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
done
|
||||
else
|
||||
log_error "Test 2 failed: No job ID found in response"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
else
|
||||
log_error "Test 2 failed: API call returned error"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 3: Using fileId parameter from an async job result
|
||||
function test_file_id() {
|
||||
separator
|
||||
log_info "Test 3: Using fileId parameter from an async job"
|
||||
|
||||
# First, we need to run an async operation to get a fileId
|
||||
ASYNC_RESPONSE="$TEST_DIR/test3_async_response.json"
|
||||
|
||||
log_info "First, submitting an async rotation to get a server-side file..."
|
||||
|
||||
# Call the endpoint with async=true
|
||||
curl -s -o "$ASYNC_RESPONSE" \
|
||||
-F "fileInput=@$TEST_PDF" \
|
||||
-F "angle=90" \
|
||||
"http://localhost:8080/api/v1/general/rotate-pdf?async=true"
|
||||
|
||||
# Extract job ID from response
|
||||
JOB_ID=$(grep -o '"jobId":"[^"]*"' "$ASYNC_RESPONSE" | cut -d':' -f2 | tr -d '"')
|
||||
|
||||
if [ -z "$JOB_ID" ]; then
|
||||
log_error "Test 3 failed: No job ID found in async response"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
|
||||
log_success "Received job ID: $JOB_ID"
|
||||
|
||||
# Wait for job to complete
|
||||
log_info "Waiting for async job to complete..."
|
||||
MAX_ATTEMPTS=10
|
||||
ATTEMPT=0
|
||||
JOB_COMPLETED=false
|
||||
|
||||
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||
((ATTEMPT++))
|
||||
sleep 2
|
||||
|
||||
# Check job status
|
||||
STATUS_FILE="$TEST_DIR/test3_job_status.json"
|
||||
if curl -s -o "$STATUS_FILE" "http://localhost:8080/api/v1/general/job/$JOB_ID"; then
|
||||
echo "Job status response:"
|
||||
cat "$STATUS_FILE"
|
||||
|
||||
if grep -q '"complete":true' "$STATUS_FILE"; then
|
||||
log_success "Async job completed successfully"
|
||||
JOB_COMPLETED=true
|
||||
break
|
||||
elif grep -q '"error":' "$STATUS_FILE"; then
|
||||
ERROR=$(grep -o '"error":"[^"]*"' "$STATUS_FILE" | cut -d':' -f2 | tr -d '"')
|
||||
log_error "Test 3 failed: Async job reported error: $ERROR"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
else
|
||||
log_info "Job still processing (attempt $ATTEMPT/$MAX_ATTEMPTS)..."
|
||||
fi
|
||||
else
|
||||
log_error "Failed to check job status"
|
||||
fi
|
||||
|
||||
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
||||
log_error "Test 3 failed: Async job did not complete in time"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$JOB_COMPLETED" = false ]; then
|
||||
log_error "Test 3 failed: Async job did not complete successfully"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
|
||||
# Now get the result file from the completed job
|
||||
RESULT_FILE="$TEST_DIR/test3_result.pdf"
|
||||
if curl -s -o "$RESULT_FILE" "http://localhost:8080/api/v1/general/job/$JOB_ID/result"; then
|
||||
if file "$RESULT_FILE" | grep -q "PDF document"; then
|
||||
log_success "Successfully downloaded result file"
|
||||
else
|
||||
log_error "Test 3 failed: Downloaded result is not a valid PDF"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
else
|
||||
log_error "Test 3 failed: Could not download result file"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
|
||||
# Now check the job result info to get fileId
|
||||
RESULT_INFO="$TEST_DIR/test3_job_info.json"
|
||||
curl -s -o "$RESULT_INFO" "http://localhost:8080/api/v1/general/job/$JOB_ID"
|
||||
|
||||
# Try to extract fileId directly from the job info
|
||||
FILE_ID=$(grep -o '"fileId":"[^"]*"' "$RESULT_INFO" | head -1 | cut -d':' -f2 | tr -d '"')
|
||||
|
||||
if [ -z "$FILE_ID" ]; then
|
||||
log_error "Test 3 failed: Could not find fileId in job result"
|
||||
echo "Job result content:"
|
||||
cat "$RESULT_INFO"
|
||||
|
||||
# Even if we couldn't find a fileId, let's try to proceed using an alternate approach
|
||||
log_warning "Falling back to alternate approach: extracting fileId from request PDFFile"
|
||||
|
||||
# Run another async job but extract fileId from the request PDFFile
|
||||
ASYNC_RESPONSE2="$TEST_DIR/test3_async_response2.json"
|
||||
|
||||
curl -vvv -s -o "$ASYNC_RESPONSE2" \
|
||||
-F "fileInput=@$TEST_PDF" \
|
||||
-F "angle=90" \
|
||||
"http://localhost:8080/api/v1/general/rotate-pdf?async=true" 2>&1 | tee "$TEST_DIR/curl_verbose.log"
|
||||
|
||||
echo "Curl verbose log for debugging:"
|
||||
cat "$TEST_DIR/curl_verbose.log"
|
||||
|
||||
# Try to get the fileId from the async response
|
||||
JOB_ID2=$(grep -o '"jobId":"[^"]*"' "$ASYNC_RESPONSE2" | cut -d':' -f2 | tr -d '"')
|
||||
|
||||
if [ -z "$JOB_ID2" ]; then
|
||||
log_error "Test 3 failed: No job ID found in second async response"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
|
||||
log_success "Received second job ID: $JOB_ID2"
|
||||
|
||||
# Wait for this job to complete as well
|
||||
sleep 5
|
||||
|
||||
# Get the job status to see if fileId is available
|
||||
RESULT_INFO2="$TEST_DIR/test3_job_info2.json"
|
||||
curl -s -o "$RESULT_INFO2" "http://localhost:8080/api/v1/general/job/$JOB_ID2"
|
||||
|
||||
# Try to extract fileId directly from the job info
|
||||
FILE_ID=$(grep -o '"fileId":"[^"]*"' "$RESULT_INFO2" | head -1 | cut -d':' -f2 | tr -d '"')
|
||||
|
||||
if [ -z "$FILE_ID" ]; then
|
||||
log_error "Test 3 failed: Could not find fileId in second job result either"
|
||||
echo "Second job result content:"
|
||||
cat "$RESULT_INFO2"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "Extracted fileId from job result: $FILE_ID"
|
||||
|
||||
# Now use the fileId to rotate the PDF again with a different angle
|
||||
RESPONSE_FILE="$TEST_DIR/fileid_response.pdf"
|
||||
|
||||
log_info "Calling rotate-pdf endpoint with fileId and angle=270..."
|
||||
|
||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o "$RESPONSE_FILE" \
|
||||
-F "fileId=$FILE_ID" \
|
||||
-F "angle=270" \
|
||||
-H "Accept: application/pdf" \
|
||||
http://localhost:8080/api/v1/general/rotate-pdf)
|
||||
|
||||
echo "Response status code: $HTTP_CODE"
|
||||
|
||||
if [[ $HTTP_CODE -ge 200 && $HTTP_CODE -lt 300 ]]; then
|
||||
# Check if response is a valid PDF
|
||||
if file "$RESPONSE_FILE" | grep -q "PDF document"; then
|
||||
log_success "Test 3 succeeded: Received valid PDF response using fileId (HTTP $HTTP_CODE)"
|
||||
((SUCCESS_COUNT++))
|
||||
# Check if it's a JSON response with an embedded PDF
|
||||
elif grep -q "result" "$RESPONSE_FILE" && grep -q "application/pdf" "$RESPONSE_FILE"; then
|
||||
log_warning "Test 3 partial: Response is a JSON wrapper instead of direct PDF (HTTP $HTTP_CODE)"
|
||||
log_info "The API returned a JSON wrapper. This will be fixed by the JobExecutorService update."
|
||||
((SUCCESS_COUNT++))
|
||||
else
|
||||
log_error "Test 3 failed: Response is neither a valid PDF nor a JSON wrapper (HTTP $HTTP_CODE)"
|
||||
echo "Response content:"
|
||||
cat "$RESPONSE_FILE"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
else
|
||||
log_error "Test 3 failed: API call with fileId returned error (HTTP $HTTP_CODE)"
|
||||
echo "Response content:"
|
||||
cat "$RESPONSE_FILE"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 4: Error handling (invalid angle)
|
||||
function test_error_handling() {
|
||||
separator
|
||||
log_info "Test 4: Error handling (invalid angle)"
|
||||
|
||||
RESPONSE_FILE="$TEST_DIR/error_response.txt"
|
||||
|
||||
log_info "Calling rotate-pdf endpoint with invalid angle=45..."
|
||||
|
||||
# Call the endpoint with an invalid angle (not multiple of 90)
|
||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o "$RESPONSE_FILE" \
|
||||
-F "fileInput=@$TEST_PDF" \
|
||||
-F "angle=45" \
|
||||
http://localhost:8080/api/v1/general/rotate-pdf)
|
||||
|
||||
# Check if we got an error response (4xx or 5xx)
|
||||
if [[ $HTTP_CODE -ge 400 ]]; then
|
||||
log_success "Test 4 succeeded: Received error response for invalid angle (HTTP $HTTP_CODE)"
|
||||
((SUCCESS_COUNT++))
|
||||
else
|
||||
log_error "Test 4 failed: Did not receive error for invalid angle (HTTP $HTTP_CODE)"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 5: Non-async operation with fileId from an async job
|
||||
function test_non_async_with_fileid() {
|
||||
separator
|
||||
log_info "Test 5: Non-async operation with fileId from an async job"
|
||||
|
||||
# First, we need to run an async operation to get a fileId
|
||||
ASYNC_RESPONSE="$TEST_DIR/test5_async_response.json"
|
||||
|
||||
log_info "First, submitting an async rotation to get a server-side file..."
|
||||
|
||||
# Call the endpoint with async=true
|
||||
curl -s -o "$ASYNC_RESPONSE" \
|
||||
-F "fileInput=@$TEST_PDF" \
|
||||
-F "angle=90" \
|
||||
"http://localhost:8080/api/v1/general/rotate-pdf?async=true"
|
||||
|
||||
# Extract job ID from response
|
||||
JOB_ID=$(grep -o '"jobId":"[^"]*"' "$ASYNC_RESPONSE" | cut -d':' -f2 | tr -d '"')
|
||||
|
||||
if [ -z "$JOB_ID" ]; then
|
||||
log_error "Test 5 failed: No job ID found in async response"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
|
||||
log_success "Received job ID: $JOB_ID"
|
||||
|
||||
# Wait for job to complete
|
||||
log_info "Waiting for async job to complete..."
|
||||
MAX_ATTEMPTS=10
|
||||
ATTEMPT=0
|
||||
JOB_COMPLETED=false
|
||||
|
||||
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||
((ATTEMPT++))
|
||||
sleep 2
|
||||
|
||||
# Check job status
|
||||
STATUS_FILE="$TEST_DIR/test5_job_status.json"
|
||||
if curl -s -o "$STATUS_FILE" "http://localhost:8080/api/v1/general/job/$JOB_ID"; then
|
||||
if grep -q '"complete":true' "$STATUS_FILE"; then
|
||||
log_success "Async job completed successfully"
|
||||
JOB_COMPLETED=true
|
||||
break
|
||||
elif grep -q '"error":' "$STATUS_FILE"; then
|
||||
ERROR=$(grep -o '"error":"[^"]*"' "$STATUS_FILE" | cut -d':' -f2 | tr -d '"')
|
||||
log_error "Test 5 failed: Async job reported error: $ERROR"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
else
|
||||
log_info "Job still processing (attempt $ATTEMPT/$MAX_ATTEMPTS)..."
|
||||
fi
|
||||
else
|
||||
log_error "Failed to check job status"
|
||||
fi
|
||||
|
||||
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
||||
log_error "Test 5 failed: Async job did not complete in time"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$JOB_COMPLETED" = false ]; then
|
||||
log_error "Test 5 failed: Async job did not complete successfully"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
|
||||
# Get the job status info
|
||||
RESULT_INFO="$TEST_DIR/test5_job_info.json"
|
||||
curl -s -o "$RESULT_INFO" "http://localhost:8080/api/v1/general/job/$JOB_ID"
|
||||
|
||||
# Try to extract fileId directly from the job info
|
||||
FILE_ID=$(grep -o '"fileId":"[^"]*"' "$RESULT_INFO" | head -1 | cut -d':' -f2 | tr -d '"')
|
||||
|
||||
if [ -z "$FILE_ID" ]; then
|
||||
log_error "Test 5 failed: Could not find fileId in job result"
|
||||
echo "Job result content:"
|
||||
cat "$RESULT_INFO"
|
||||
((FAILURE_COUNT++))
|
||||
return
|
||||
fi
|
||||
|
||||
log_success "Extracted fileId from job result: $FILE_ID"
|
||||
|
||||
# Now use the fileId to rotate the PDF with a non-async operation
|
||||
RESPONSE_FILE="$TEST_DIR/test5_response.pdf"
|
||||
|
||||
log_info "Calling rotate-pdf endpoint with fileId and angle=180 (non-async)..."
|
||||
|
||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o "$RESPONSE_FILE" \
|
||||
-F "fileId=$FILE_ID" \
|
||||
-F "angle=180" \
|
||||
-H "Accept: application/pdf" \
|
||||
http://localhost:8080/api/v1/general/rotate-pdf)
|
||||
|
||||
echo "Response status code: $HTTP_CODE"
|
||||
|
||||
if [[ $HTTP_CODE -ge 200 && $HTTP_CODE -lt 300 ]]; then
|
||||
# Check if response is a valid PDF
|
||||
if file "$RESPONSE_FILE" | grep -q "PDF document"; then
|
||||
log_success "Test 5 succeeded: Received valid PDF response using fileId in non-async mode (HTTP $HTTP_CODE)"
|
||||
((SUCCESS_COUNT++))
|
||||
else
|
||||
log_error "Test 5 failed: Response is not a valid PDF (HTTP $HTTP_CODE)"
|
||||
echo "Response content type:"
|
||||
file "$RESPONSE_FILE"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
else
|
||||
log_error "Test 5 failed: API call with fileId returned error (HTTP $HTTP_CODE)"
|
||||
echo "Response content:"
|
||||
cat "$RESPONSE_FILE"
|
||||
((FAILURE_COUNT++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Run tests
|
||||
check_service || exit 1
|
||||
|
||||
echo "Starting Test 1"
|
||||
test_sync_file_upload
|
||||
echo "Test 1 completed"
|
||||
|
||||
echo "Starting Test 2"
|
||||
test_async_file_upload
|
||||
echo "Test 2 completed"
|
||||
|
||||
echo "Starting Test 3"
|
||||
test_file_id
|
||||
echo "Test 3 completed"
|
||||
|
||||
echo "Starting Test 4"
|
||||
test_error_handling
|
||||
echo "Test 4 completed"
|
||||
|
||||
echo "Starting Test 5"
|
||||
test_non_async_with_fileid
|
||||
echo "Test 5 completed"
|
||||
|
||||
# Calculate test duration
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
|
||||
# Generate report
|
||||
separator
|
||||
echo -e "${BLUE}==== AutoJobPostMapping Test Report ====${NC}"
|
||||
echo -e "Test duration: ${DURATION} seconds"
|
||||
echo -e "Tests passed: ${GREEN}${SUCCESS_COUNT}${NC}"
|
||||
echo -e "Tests failed: ${RED}${FAILURE_COUNT}${NC}"
|
||||
echo -e "Total tests: $((SUCCESS_COUNT + FAILURE_COUNT))"
|
||||
echo -e "Success rate: $(( (SUCCESS_COUNT * 100) / (SUCCESS_COUNT + FAILURE_COUNT) ))%"
|
||||
|
||||
if [ $FAILURE_COUNT -eq 0 ]; then
|
||||
echo -e "\n${GREEN}All tests passed successfully!${NC}"
|
||||
else
|
||||
echo -e "\n${RED}Some tests failed. Check the logs above for details.${NC}"
|
||||
fi
|
||||
separator
|
||||
|
||||
# Clean up
|
||||
# Uncomment the following line to keep test files for inspection
|
||||
# rm -rf "$TEST_DIR"
|
||||
|
||||
echo "Test files are available in $TEST_DIR"
|
Loading…
x
Reference in New Issue
Block a user