Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7874d18f32 | ||
|
|
1e968c79f6 | ||
|
|
e2b88cf8b4 | ||
|
|
da49b50d27 | ||
|
|
970679640d | ||
|
|
18f571291b | ||
|
|
0abcc0ccb8 | ||
|
|
a99c104053 | ||
|
|
95fbd4647d | ||
|
|
8d4f5f16cb | ||
|
|
2ee43b2070 | ||
|
|
02aad40aa0 | ||
|
|
0328333d81 |
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(./gradlew:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(rm:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(if [ -d logs ])",
|
||||||
|
"Bash(then ls -la logs/)",
|
||||||
|
"Bash(fi)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(./testing/cucumber/features/environment.py -v)",
|
||||||
|
"Bash(./testing/cucumber/features/autojob.feature -v)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,9 +56,9 @@ jobs:
|
|||||||
build/reports/tests/
|
build/reports/tests/
|
||||||
build/test-results/
|
build/test-results/
|
||||||
build/reports/problems/
|
build/reports/problems/
|
||||||
/common/build/reports/tests/
|
common/build/reports/tests/
|
||||||
/common/build/test-results/
|
common/build/test-results/
|
||||||
/common/build/reports/problems/
|
common/build/reports/problems/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
check-licence:
|
check-licence:
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ dependencyManagement {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
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.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||||
implementation 'com.fathzer:javaluator:3.0.6'
|
implementation 'com.fathzer:javaluator:3.0.6'
|
||||||
implementation 'com.posthog.java:posthog:1.2.0'
|
implementation 'com.posthog.java:posthog:1.2.0'
|
||||||
@@ -48,5 +51,14 @@ dependencies {
|
|||||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||||
|
testImplementation 'org.mockito:mockito-core:5.18.0'
|
||||||
|
testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0'
|
||||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events "passed", "skipped", "failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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"};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom timeout in milliseconds for this specific job. If not specified, the default system
|
||||||
|
* timeout will be used.
|
||||||
|
*/
|
||||||
|
long timeout() default -1;
|
||||||
|
|
||||||
|
/** Maximum number of times to retry the job on failure. Default is 1 (no retries). */
|
||||||
|
int retryCount() default 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to track and report progress for this job. If enabled, the job will send progress
|
||||||
|
* updates through WebSocket.
|
||||||
|
*/
|
||||||
|
boolean trackProgress() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this job can be queued when system resources are limited. If enabled, jobs will be
|
||||||
|
* queued instead of rejected when the system is under high load. The queue size is dynamically
|
||||||
|
* adjusted based on available memory and CPU resources.
|
||||||
|
*/
|
||||||
|
boolean queueable() default false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional resource weight of this job (1-100). Higher values indicate more resource-intensive
|
||||||
|
* jobs that may need stricter queuing. Default is 50 (medium weight).
|
||||||
|
*/
|
||||||
|
int resourceWeight() default 50;
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package stirling.software.common.aop;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
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.controller.WebSocketProgressController;
|
||||||
|
import stirling.software.common.model.api.PDFFile;
|
||||||
|
import stirling.software.common.model.job.JobProgress;
|
||||||
|
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;
|
||||||
|
private final WebSocketProgressController webSocketSender;
|
||||||
|
|
||||||
|
@Around("@annotation(autoJobPostMapping)")
|
||||||
|
public Object wrapWithJobExecution(
|
||||||
|
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
|
||||||
|
// Extract parameters from the request and annotation
|
||||||
|
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||||
|
long timeout = autoJobPostMapping.timeout();
|
||||||
|
int retryCount = autoJobPostMapping.retryCount();
|
||||||
|
boolean trackProgress = autoJobPostMapping.trackProgress();
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"AutoJobPostMapping execution with async={}, timeout={}, retryCount={}, trackProgress={}",
|
||||||
|
async,
|
||||||
|
timeout > 0 ? timeout : "default",
|
||||||
|
retryCount,
|
||||||
|
trackProgress);
|
||||||
|
|
||||||
|
// Inspect and possibly mutate arguments
|
||||||
|
Object[] args = joinPoint.getArgs();
|
||||||
|
|
||||||
|
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 (async && 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract queueable and resourceWeight parameters
|
||||||
|
boolean queueable = autoJobPostMapping.queueable();
|
||||||
|
int resourceWeight = autoJobPostMapping.resourceWeight();
|
||||||
|
|
||||||
|
// Integrate with the enhanced JobExecutorService
|
||||||
|
if (retryCount <= 1) {
|
||||||
|
// No retries needed, simple execution
|
||||||
|
return jobExecutorService.runJobGeneric(
|
||||||
|
async,
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
if (trackProgress && async) {
|
||||||
|
String jobId = (String) request.getAttribute("jobId");
|
||||||
|
if (jobId != null) {
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId,
|
||||||
|
new JobProgress(
|
||||||
|
jobId,
|
||||||
|
"Processing",
|
||||||
|
50,
|
||||||
|
"Executing operation"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return joinPoint.proceed(args);
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
log.error(
|
||||||
|
"AutoJobAspect caught exception during job execution: {}",
|
||||||
|
ex.getMessage(),
|
||||||
|
ex);
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
queueable,
|
||||||
|
resourceWeight);
|
||||||
|
} else {
|
||||||
|
// Use retry logic
|
||||||
|
return executeWithRetries(
|
||||||
|
joinPoint,
|
||||||
|
args,
|
||||||
|
async,
|
||||||
|
timeout,
|
||||||
|
retryCount,
|
||||||
|
trackProgress,
|
||||||
|
queueable,
|
||||||
|
resourceWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object executeWithRetries(
|
||||||
|
ProceedingJoinPoint joinPoint,
|
||||||
|
Object[] args,
|
||||||
|
boolean async,
|
||||||
|
long timeout,
|
||||||
|
int maxRetries,
|
||||||
|
boolean trackProgress,
|
||||||
|
boolean queueable,
|
||||||
|
int resourceWeight) {
|
||||||
|
|
||||||
|
AtomicInteger attempts = new AtomicInteger(0);
|
||||||
|
// Use AtomicReference to make the jobId effectively final for lambda usage
|
||||||
|
AtomicReference<String> jobIdRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
return jobExecutorService.runJobGeneric(
|
||||||
|
async,
|
||||||
|
() -> {
|
||||||
|
int currentAttempt = attempts.incrementAndGet();
|
||||||
|
try {
|
||||||
|
if (trackProgress && async) {
|
||||||
|
// Only get the jobId once and store it in the AtomicReference
|
||||||
|
if (jobIdRef.get() == null) {
|
||||||
|
jobIdRef.set(getJobIdFromContext());
|
||||||
|
}
|
||||||
|
String jobId = jobIdRef.get();
|
||||||
|
if (jobId != null) {
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId,
|
||||||
|
new JobProgress(
|
||||||
|
jobId,
|
||||||
|
"Started",
|
||||||
|
0,
|
||||||
|
"Executing (attempt "
|
||||||
|
+ currentAttempt
|
||||||
|
+ " of "
|
||||||
|
+ Math.max(1, maxRetries)
|
||||||
|
+ ")"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return joinPoint.proceed(args);
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
log.error(
|
||||||
|
"AutoJobAspect caught exception during job execution (attempt {}/{}): {}",
|
||||||
|
currentAttempt,
|
||||||
|
Math.max(1, maxRetries),
|
||||||
|
ex.getMessage(),
|
||||||
|
ex);
|
||||||
|
|
||||||
|
// Check if we should retry
|
||||||
|
if (currentAttempt < maxRetries) {
|
||||||
|
log.info(
|
||||||
|
"Retrying operation, attempt {}/{}",
|
||||||
|
currentAttempt + 1,
|
||||||
|
maxRetries);
|
||||||
|
String jobId = jobIdRef.get();
|
||||||
|
if (trackProgress && async && jobId != null) {
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId,
|
||||||
|
new JobProgress(
|
||||||
|
jobId,
|
||||||
|
"Retrying",
|
||||||
|
(int) (currentAttempt * 100.0 / maxRetries),
|
||||||
|
"Retry attempt "
|
||||||
|
+ (currentAttempt + 1)
|
||||||
|
+ " of "
|
||||||
|
+ maxRetries));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simple exponential backoff
|
||||||
|
Thread.sleep(100 * currentAttempt);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive call to retry
|
||||||
|
return executeWithRetries(
|
||||||
|
joinPoint,
|
||||||
|
args,
|
||||||
|
async,
|
||||||
|
timeout,
|
||||||
|
maxRetries,
|
||||||
|
trackProgress,
|
||||||
|
queueable,
|
||||||
|
resourceWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more retries, throw the exception
|
||||||
|
throw new RuntimeException("Job failed: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeout,
|
||||||
|
queueable,
|
||||||
|
resourceWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the job ID from the context (if this is an async job)
|
||||||
|
private String getJobIdFromContext() {
|
||||||
|
try {
|
||||||
|
return (String) request.getAttribute("jobId");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Could not retrieve job ID from context: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,114 @@
|
|||||||
|
package stirling.software.common.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
import stirling.software.common.service.FileStorage;
|
||||||
|
import stirling.software.common.service.TaskManager;
|
||||||
|
|
||||||
|
/** REST controller for job-related endpoints */
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class JobController {
|
||||||
|
|
||||||
|
private final TaskManager taskManager;
|
||||||
|
private final FileStorage fileStorage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = taskManager.getJobResult(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 = taskManager.getJobResult(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get statistics about jobs in the system
|
||||||
|
*
|
||||||
|
* @return Job statistics
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/v1/general/job/stats")
|
||||||
|
public ResponseEntity<JobStats> getJobStats() {
|
||||||
|
JobStats stats = taskManager.getJobStats();
|
||||||
|
return ResponseEntity.ok(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger cleanup of old jobs
|
||||||
|
*
|
||||||
|
* @return A response indicating how many jobs were cleaned up
|
||||||
|
*/
|
||||||
|
@PostMapping("/api/v1/general/job/cleanup")
|
||||||
|
public ResponseEntity<?> cleanupOldJobs() {
|
||||||
|
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
||||||
|
taskManager.cleanupOldJobs();
|
||||||
|
int afterCount = taskManager.getJobStats().getTotalJobs();
|
||||||
|
int removedCount = beforeCount - afterCount;
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"message", "Cleanup complete",
|
||||||
|
"removedJobs", removedCount,
|
||||||
|
"remainingJobs", afterCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -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 {
|
public class PDFFile {
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "The input PDF file",
|
description = "The input PDF file",
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
|
||||||
contentMediaType = "application/pdf",
|
contentMediaType = "application/pdf",
|
||||||
format = "binary")
|
format = "binary")
|
||||||
private MultipartFile fileInput;
|
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,94 @@
|
|||||||
|
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,43 @@
|
|||||||
|
package stirling.software.common.model.job;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/** Represents statistics about jobs in the system */
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class JobStats {
|
||||||
|
|
||||||
|
/** Total number of jobs (active and completed) */
|
||||||
|
private int totalJobs;
|
||||||
|
|
||||||
|
/** Number of active (incomplete) jobs */
|
||||||
|
private int activeJobs;
|
||||||
|
|
||||||
|
/** Number of completed jobs */
|
||||||
|
private int completedJobs;
|
||||||
|
|
||||||
|
/** Number of failed jobs */
|
||||||
|
private int failedJobs;
|
||||||
|
|
||||||
|
/** Number of successful jobs */
|
||||||
|
private int successfulJobs;
|
||||||
|
|
||||||
|
/** Number of jobs with file results */
|
||||||
|
private int fileResultJobs;
|
||||||
|
|
||||||
|
/** The oldest active job's creation timestamp */
|
||||||
|
private LocalDateTime oldestActiveJobTime;
|
||||||
|
|
||||||
|
/** The newest active job's creation timestamp */
|
||||||
|
private LocalDateTime newestActiveJobTime;
|
||||||
|
|
||||||
|
/** The average processing time for completed jobs in milliseconds */
|
||||||
|
private long averageProcessingTimeMs;
|
||||||
|
}
|
||||||
@@ -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,465 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
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 jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import stirling.software.common.util.ExecutorFactory;
|
||||||
|
|
||||||
|
/** Service for executing jobs asynchronously or synchronously */
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class JobExecutorService {
|
||||||
|
|
||||||
|
private final TaskManager taskManager;
|
||||||
|
private final WebSocketProgressController webSocketSender;
|
||||||
|
private final FileStorage fileStorage;
|
||||||
|
private final HttpServletRequest request;
|
||||||
|
private final ResourceMonitor resourceMonitor;
|
||||||
|
private final JobQueue jobQueue;
|
||||||
|
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||||
|
private final long effectiveTimeoutMs;
|
||||||
|
|
||||||
|
public JobExecutorService(
|
||||||
|
TaskManager taskManager,
|
||||||
|
WebSocketProgressController webSocketSender,
|
||||||
|
FileStorage fileStorage,
|
||||||
|
HttpServletRequest request,
|
||||||
|
ResourceMonitor resourceMonitor,
|
||||||
|
JobQueue jobQueue,
|
||||||
|
@Value("${spring.mvc.async.request-timeout:1200000}") long asyncRequestTimeoutMs,
|
||||||
|
@Value("${server.servlet.session.timeout:30m}") String sessionTimeout) {
|
||||||
|
this.taskManager = taskManager;
|
||||||
|
this.webSocketSender = webSocketSender;
|
||||||
|
this.fileStorage = fileStorage;
|
||||||
|
this.request = request;
|
||||||
|
this.resourceMonitor = resourceMonitor;
|
||||||
|
this.jobQueue = jobQueue;
|
||||||
|
|
||||||
|
// Parse session timeout and calculate effective timeout once during initialization
|
||||||
|
long sessionTimeoutMs = parseSessionTimeout(sessionTimeout);
|
||||||
|
this.effectiveTimeoutMs = Math.min(asyncRequestTimeoutMs, sessionTimeoutMs);
|
||||||
|
log.debug(
|
||||||
|
"Job executor configured with effective timeout of {} ms", this.effectiveTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
return runJobGeneric(async, work, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a job either asynchronously or synchronously with a custom timeout
|
||||||
|
*
|
||||||
|
* @param async Whether to run the job asynchronously
|
||||||
|
* @param work The work to be done
|
||||||
|
* @param customTimeoutMs Custom timeout in milliseconds, or -1 to use the default
|
||||||
|
* @return The response
|
||||||
|
*/
|
||||||
|
public ResponseEntity<?> runJobGeneric(
|
||||||
|
boolean async, Supplier<Object> work, long customTimeoutMs) {
|
||||||
|
return runJobGeneric(async, work, customTimeoutMs, false, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a job either asynchronously or synchronously with custom parameters
|
||||||
|
*
|
||||||
|
* @param async Whether to run the job asynchronously
|
||||||
|
* @param work The work to be done
|
||||||
|
* @param customTimeoutMs Custom timeout in milliseconds, or -1 to use the default
|
||||||
|
* @param queueable Whether this job can be queued when system resources are limited
|
||||||
|
* @param resourceWeight The resource weight of this job (1-100)
|
||||||
|
* @return The response
|
||||||
|
*/
|
||||||
|
public ResponseEntity<?> runJobGeneric(
|
||||||
|
boolean async,
|
||||||
|
Supplier<Object> work,
|
||||||
|
long customTimeoutMs,
|
||||||
|
boolean queueable,
|
||||||
|
int resourceWeight) {
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
// Store the job ID in the request for potential use by other components
|
||||||
|
if (request != null) {
|
||||||
|
request.setAttribute("jobId", jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which timeout to use
|
||||||
|
long timeoutToUse = customTimeoutMs > 0 ? customTimeoutMs : effectiveTimeoutMs;
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Running job with ID: {}, async: {}, timeout: {}ms, queueable: {}, weight: {}",
|
||||||
|
jobId,
|
||||||
|
async,
|
||||||
|
timeoutToUse,
|
||||||
|
queueable,
|
||||||
|
resourceWeight);
|
||||||
|
|
||||||
|
// Check if we need to queue this job based on resource availability
|
||||||
|
boolean shouldQueue =
|
||||||
|
queueable
|
||||||
|
&& async
|
||||||
|
&& // Only async jobs can be queued
|
||||||
|
resourceMonitor.shouldQueueJob(resourceWeight);
|
||||||
|
|
||||||
|
if (shouldQueue) {
|
||||||
|
// Queue the job instead of executing immediately
|
||||||
|
log.debug(
|
||||||
|
"Queueing job {} due to resource constraints (weight: {})",
|
||||||
|
jobId,
|
||||||
|
resourceWeight);
|
||||||
|
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Create a specialized wrapper that updates the TaskManager
|
||||||
|
Supplier<Object> wrappedWork =
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
Object result = work.get();
|
||||||
|
processJobResult(jobId, result);
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(
|
||||||
|
"Error executing queued job {}: {}", jobId, e.getMessage(), e);
|
||||||
|
taskManager.setError(jobId, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Queue the job and get the future
|
||||||
|
CompletableFuture<ResponseEntity<?>> future =
|
||||||
|
jobQueue.queueJob(jobId, resourceWeight, wrappedWork, timeoutToUse);
|
||||||
|
|
||||||
|
// Return immediately with job ID
|
||||||
|
return ResponseEntity.ok().body(new JobResponse<>(true, jobId, null));
|
||||||
|
} else if (async) {
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
webSocketSender.sendProgress(jobId, new JobProgress(jobId, "Started", 0, "Running"));
|
||||||
|
|
||||||
|
executor.execute(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
log.debug(
|
||||||
|
"Running async job {} with timeout {} ms", jobId, timeoutToUse);
|
||||||
|
|
||||||
|
// Execute with timeout
|
||||||
|
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
||||||
|
processJobResult(jobId, result);
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId, new JobProgress(jobId, "Done", 100, "Complete"));
|
||||||
|
} catch (TimeoutException te) {
|
||||||
|
log.error("Job {} timed out after {} ms", jobId, timeoutToUse);
|
||||||
|
taskManager.setError(jobId, "Job timed out");
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId, new JobProgress(jobId, "Error", 100, "Job timed out"));
|
||||||
|
} 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 {
|
||||||
|
log.debug("Running sync job with timeout {} ms", timeoutToUse);
|
||||||
|
|
||||||
|
// Execute with timeout
|
||||||
|
Object result = executeWithTimeout(() -> work.get(), timeoutToUse);
|
||||||
|
|
||||||
|
// 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 (TimeoutException te) {
|
||||||
|
log.error("Synchronous job timed out after {} ms", timeoutToUse);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", "Job timed out after " + timeoutToUse + " ms"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
||||||
|
// Construct a JSON error response
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", "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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse session timeout string (e.g., "30m", "1h") to milliseconds
|
||||||
|
*
|
||||||
|
* @param timeout The timeout string
|
||||||
|
* @return The timeout in milliseconds
|
||||||
|
*/
|
||||||
|
private long parseSessionTimeout(String timeout) {
|
||||||
|
if (timeout == null || timeout.isEmpty()) {
|
||||||
|
return 30 * 60 * 1000; // Default: 30 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String value = timeout.replaceAll("[^\\d.]", "");
|
||||||
|
String unit = timeout.replaceAll("[\\d.]", "");
|
||||||
|
|
||||||
|
double numericValue = Double.parseDouble(value);
|
||||||
|
|
||||||
|
return switch (unit.toLowerCase()) {
|
||||||
|
case "s" -> (long) (numericValue * 1000);
|
||||||
|
case "m" -> (long) (numericValue * 60 * 1000);
|
||||||
|
case "h" -> (long) (numericValue * 60 * 60 * 1000);
|
||||||
|
case "d" -> (long) (numericValue * 24 * 60 * 60 * 1000);
|
||||||
|
default -> (long) (numericValue * 60 * 1000); // Default to minutes
|
||||||
|
};
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Could not parse session timeout '{}', using default", timeout);
|
||||||
|
return 30 * 60 * 1000; // Default: 30 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a supplier with a timeout
|
||||||
|
*
|
||||||
|
* @param supplier The supplier to execute
|
||||||
|
* @param timeoutMs The timeout in milliseconds
|
||||||
|
* @return The result from the supplier
|
||||||
|
* @throws TimeoutException If the execution times out
|
||||||
|
* @throws Exception If the supplier throws an exception
|
||||||
|
*/
|
||||||
|
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs)
|
||||||
|
throws TimeoutException, Exception {
|
||||||
|
java.util.concurrent.CompletableFuture<T> future =
|
||||||
|
java.util.concurrent.CompletableFuture.supplyAsync(supplier);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new TimeoutException("Execution timed out after " + timeoutMs + " ms");
|
||||||
|
} catch (java.util.concurrent.ExecutionException e) {
|
||||||
|
throw (Exception) e.getCause();
|
||||||
|
} catch (java.util.concurrent.CancellationException e) {
|
||||||
|
throw new Exception("Execution was cancelled", e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new Exception("Execution was interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.controller.WebSocketProgressController;
|
||||||
|
import stirling.software.common.model.job.JobProgress;
|
||||||
|
import stirling.software.common.util.ExecutorFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages a queue of jobs with dynamic sizing based on system resources. Used when system resources
|
||||||
|
* are limited to prevent overloading.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class JobQueue {
|
||||||
|
|
||||||
|
private final ResourceMonitor resourceMonitor;
|
||||||
|
private final WebSocketProgressController webSocketSender;
|
||||||
|
|
||||||
|
@Value("${stirling.job.queue.base-capacity:10}")
|
||||||
|
private int baseQueueCapacity = 10;
|
||||||
|
|
||||||
|
@Value("${stirling.job.queue.min-capacity:2}")
|
||||||
|
private int minQueueCapacity = 2;
|
||||||
|
|
||||||
|
@Value("${stirling.job.queue.check-interval-ms:1000}")
|
||||||
|
private long queueCheckIntervalMs = 1000;
|
||||||
|
|
||||||
|
@Value("${stirling.job.queue.max-wait-time-ms:600000}")
|
||||||
|
private long maxWaitTimeMs = 600000; // 10 minutes
|
||||||
|
|
||||||
|
private BlockingQueue<QueuedJob> jobQueue;
|
||||||
|
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||||
|
|
||||||
|
private boolean shuttingDown = false;
|
||||||
|
|
||||||
|
@Getter private int rejectedJobs = 0;
|
||||||
|
|
||||||
|
@Getter private int totalQueuedJobs = 0;
|
||||||
|
|
||||||
|
@Getter private int currentQueueSize = 0;
|
||||||
|
|
||||||
|
/** Represents a job waiting in the queue. */
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class QueuedJob {
|
||||||
|
private final String jobId;
|
||||||
|
private final int resourceWeight;
|
||||||
|
private final Supplier<Object> work;
|
||||||
|
private final long timeoutMs;
|
||||||
|
private final Instant queuedAt;
|
||||||
|
private CompletableFuture<ResponseEntity<?>> future;
|
||||||
|
private volatile boolean cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JobQueue(ResourceMonitor resourceMonitor, WebSocketProgressController webSocketSender) {
|
||||||
|
this.resourceMonitor = resourceMonitor;
|
||||||
|
this.webSocketSender = webSocketSender;
|
||||||
|
|
||||||
|
// Initialize with dynamic capacity
|
||||||
|
int capacity =
|
||||||
|
resourceMonitor.calculateDynamicQueueCapacity(baseQueueCapacity, minQueueCapacity);
|
||||||
|
this.jobQueue = new LinkedBlockingQueue<>(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initialize() {
|
||||||
|
log.debug(
|
||||||
|
"Starting job queue with base capacity {}, min capacity {}",
|
||||||
|
baseQueueCapacity,
|
||||||
|
minQueueCapacity);
|
||||||
|
|
||||||
|
// Periodically process the job queue
|
||||||
|
scheduler.scheduleWithFixedDelay(
|
||||||
|
this::processQueue, 0, queueCheckIntervalMs, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
// Periodically update queue capacity based on resource usage
|
||||||
|
scheduler.scheduleWithFixedDelay(
|
||||||
|
this::updateQueueCapacity,
|
||||||
|
10000, // Initial delay
|
||||||
|
30000, // 30 second interval
|
||||||
|
TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
log.info("Shutting down job queue");
|
||||||
|
shuttingDown = true;
|
||||||
|
|
||||||
|
// Complete any futures that are still waiting
|
||||||
|
jobMap.forEach(
|
||||||
|
(id, job) -> {
|
||||||
|
if (!job.future.isDone()) {
|
||||||
|
job.future.completeExceptionally(
|
||||||
|
new RuntimeException("Server shutting down, job cancelled"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
jobExecutor.shutdownNow();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Job queue shutdown complete. Stats: total={}, rejected={}",
|
||||||
|
totalQueuedJobs,
|
||||||
|
rejectedJobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues a job for execution when resources permit.
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @param resourceWeight The resource weight of the job (1-100)
|
||||||
|
* @param work The work to be done
|
||||||
|
* @param timeoutMs The timeout in milliseconds
|
||||||
|
* @return A CompletableFuture that will complete when the job is executed
|
||||||
|
*/
|
||||||
|
public CompletableFuture<ResponseEntity<?>> queueJob(
|
||||||
|
String jobId, int resourceWeight, Supplier<Object> work, long timeoutMs) {
|
||||||
|
|
||||||
|
// Create a CompletableFuture to track this job's completion
|
||||||
|
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
|
||||||
|
|
||||||
|
// Create the queued job
|
||||||
|
QueuedJob job =
|
||||||
|
new QueuedJob(jobId, resourceWeight, work, timeoutMs, Instant.now(), future, false);
|
||||||
|
|
||||||
|
// Store in our map for lookup
|
||||||
|
jobMap.put(jobId, job);
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
totalQueuedJobs++;
|
||||||
|
currentQueueSize = jobQueue.size();
|
||||||
|
|
||||||
|
// Try to add to the queue
|
||||||
|
try {
|
||||||
|
boolean added = jobQueue.offer(job, 5, TimeUnit.SECONDS);
|
||||||
|
if (!added) {
|
||||||
|
log.warn("Queue full, rejecting job {}", jobId);
|
||||||
|
rejectedJobs++;
|
||||||
|
future.completeExceptionally(
|
||||||
|
new RuntimeException("Job queue full, please try again later"));
|
||||||
|
jobMap.remove(jobId);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Job {} queued for execution (weight: {}, queue size: {})",
|
||||||
|
jobId,
|
||||||
|
resourceWeight,
|
||||||
|
jobQueue.size());
|
||||||
|
|
||||||
|
// Notify client via WebSocket that job is queued
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId, new JobProgress(jobId, "Queued", 0, "Waiting in queue for resources"));
|
||||||
|
|
||||||
|
return future;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
future.completeExceptionally(new RuntimeException("Job queue interrupted"));
|
||||||
|
jobMap.remove(jobId);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current capacity of the job queue.
|
||||||
|
*
|
||||||
|
* @return The current capacity
|
||||||
|
*/
|
||||||
|
public int getQueueCapacity() {
|
||||||
|
return ((LinkedBlockingQueue<QueuedJob>) jobQueue).remainingCapacity() + jobQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the capacity of the job queue based on available system resources. */
|
||||||
|
private void updateQueueCapacity() {
|
||||||
|
try {
|
||||||
|
// Calculate new capacity
|
||||||
|
int newCapacity =
|
||||||
|
resourceMonitor.calculateDynamicQueueCapacity(
|
||||||
|
baseQueueCapacity, minQueueCapacity);
|
||||||
|
|
||||||
|
int currentCapacity = getQueueCapacity();
|
||||||
|
if (newCapacity != currentCapacity) {
|
||||||
|
log.debug(
|
||||||
|
"Updating job queue capacity from {} to {}", currentCapacity, newCapacity);
|
||||||
|
|
||||||
|
// Create new queue with updated capacity
|
||||||
|
BlockingQueue<QueuedJob> newQueue = new LinkedBlockingQueue<>(newCapacity);
|
||||||
|
|
||||||
|
// Transfer jobs from old queue to new queue
|
||||||
|
jobQueue.drainTo(newQueue);
|
||||||
|
jobQueue = newQueue;
|
||||||
|
|
||||||
|
currentQueueSize = jobQueue.size();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error updating queue capacity: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Processes jobs in the queue, executing them when resources permit. */
|
||||||
|
private void processQueue() {
|
||||||
|
if (shuttingDown || jobQueue.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current resource status
|
||||||
|
ResourceMonitor.ResourceStatus status = resourceMonitor.getCurrentStatus().get();
|
||||||
|
|
||||||
|
// Check if we should execute any jobs
|
||||||
|
boolean canExecuteJobs = (status != ResourceMonitor.ResourceStatus.CRITICAL);
|
||||||
|
|
||||||
|
if (!canExecuteJobs) {
|
||||||
|
// Under critical load, don't execute any jobs
|
||||||
|
log.debug("System under critical load, delaying job execution");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get jobs from the queue, up to a limit based on resource availability
|
||||||
|
int jobsToProcess =
|
||||||
|
Math.max(
|
||||||
|
1,
|
||||||
|
switch (status) {
|
||||||
|
case OK -> 3;
|
||||||
|
case WARNING -> 1;
|
||||||
|
case CRITICAL -> 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int i = 0; i < jobsToProcess && !jobQueue.isEmpty(); i++) {
|
||||||
|
QueuedJob job = jobQueue.poll();
|
||||||
|
if (job == null) break;
|
||||||
|
|
||||||
|
// Check if it's been waiting too long
|
||||||
|
long waitTimeMs = Instant.now().toEpochMilli() - job.queuedAt.toEpochMilli();
|
||||||
|
if (waitTimeMs > maxWaitTimeMs) {
|
||||||
|
log.warn(
|
||||||
|
"Job {} exceeded maximum wait time ({} ms), executing anyway",
|
||||||
|
job.jobId,
|
||||||
|
waitTimeMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from our map
|
||||||
|
jobMap.remove(job.jobId);
|
||||||
|
currentQueueSize = jobQueue.size();
|
||||||
|
|
||||||
|
// Notify via WebSocket
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
job.jobId,
|
||||||
|
new JobProgress(job.jobId, "Processing", 10, "Starting job execution"));
|
||||||
|
|
||||||
|
// Execute the job
|
||||||
|
executeJob(job);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error processing job queue: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a job from the queue.
|
||||||
|
*
|
||||||
|
* @param job The job to execute
|
||||||
|
*/
|
||||||
|
private void executeJob(QueuedJob job) {
|
||||||
|
if (job.cancelled) {
|
||||||
|
log.debug("Job {} was cancelled, not executing", job.jobId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobExecutor.execute(
|
||||||
|
() -> {
|
||||||
|
log.debug("Executing queued job {} (queued at {})", job.jobId, job.queuedAt);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute with timeout
|
||||||
|
Object result = executeWithTimeout(job.work, job.timeoutMs);
|
||||||
|
|
||||||
|
// Process the result
|
||||||
|
if (result instanceof ResponseEntity) {
|
||||||
|
job.future.complete((ResponseEntity<?>) result);
|
||||||
|
} else {
|
||||||
|
job.future.complete(ResponseEntity.ok(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update WebSocket
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
job.jobId,
|
||||||
|
new JobProgress(
|
||||||
|
job.jobId, "Complete", 100, "Job completed successfully"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(
|
||||||
|
"Error executing queued job {}: {}", job.jobId, e.getMessage(), e);
|
||||||
|
job.future.completeExceptionally(e);
|
||||||
|
|
||||||
|
// Update WebSocket
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
job.jobId,
|
||||||
|
new JobProgress(
|
||||||
|
job.jobId, "Error", 100, "Job failed: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a supplier with a timeout.
|
||||||
|
*
|
||||||
|
* @param supplier The supplier to execute
|
||||||
|
* @param timeoutMs The timeout in milliseconds
|
||||||
|
* @return The result from the supplier
|
||||||
|
* @throws Exception If there is an execution error
|
||||||
|
*/
|
||||||
|
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs) throws Exception {
|
||||||
|
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (timeoutMs <= 0) {
|
||||||
|
// No timeout
|
||||||
|
return future.join();
|
||||||
|
} else {
|
||||||
|
// With timeout
|
||||||
|
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new TimeoutException("Job timed out after " + timeoutMs + "ms");
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw (Exception) e.getCause();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new InterruptedException("Job was interrupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a job is queued.
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @return true if the job is queued
|
||||||
|
*/
|
||||||
|
public boolean isJobQueued(String jobId) {
|
||||||
|
return jobMap.containsKey(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current position of a job in the queue.
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @return The position (0-based) or -1 if not found
|
||||||
|
*/
|
||||||
|
public int getJobPosition(String jobId) {
|
||||||
|
if (!jobMap.containsKey(jobId)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count positions
|
||||||
|
int position = 0;
|
||||||
|
for (QueuedJob job : jobQueue) {
|
||||||
|
if (job.jobId.equals(jobId)) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
position++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find it in the queue but it's in the map,
|
||||||
|
// it might be executing already
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a queued job.
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @return true if the job was cancelled, false if not found
|
||||||
|
*/
|
||||||
|
public boolean cancelJob(String jobId) {
|
||||||
|
QueuedJob job = jobMap.remove(jobId);
|
||||||
|
if (job != null) {
|
||||||
|
job.cancelled = true;
|
||||||
|
job.future.completeExceptionally(new RuntimeException("Job cancelled by user"));
|
||||||
|
|
||||||
|
// Try to remove from queue if it's still there
|
||||||
|
jobQueue.remove(job);
|
||||||
|
currentQueueSize = jobQueue.size();
|
||||||
|
|
||||||
|
log.debug("Job {} cancelled", jobId);
|
||||||
|
|
||||||
|
// Update WebSocket
|
||||||
|
webSocketSender.sendProgress(
|
||||||
|
jobId, new JobProgress(jobId, "Cancelled", 100, "Job cancelled by user"));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get queue statistics.
|
||||||
|
*
|
||||||
|
* @return A map containing queue statistics
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getQueueStats() {
|
||||||
|
return Map.of(
|
||||||
|
"queuedJobs", jobQueue.size(),
|
||||||
|
"queueCapacity", getQueueCapacity(),
|
||||||
|
"totalQueuedJobs", totalQueuedJobs,
|
||||||
|
"rejectedJobs", rejectedJobs,
|
||||||
|
"resourceStatus", resourceMonitor.getCurrentStatus().get().name());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.MemoryMXBean;
|
||||||
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitors system resources (CPU, memory) to inform job scheduling decisions. Provides information
|
||||||
|
* about available resources to prevent overloading the system.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class ResourceMonitor {
|
||||||
|
|
||||||
|
@Value("${stirling.resource.memory.critical-threshold:0.9}")
|
||||||
|
private double memoryCriticalThreshold = 0.9; // 90% usage is critical
|
||||||
|
|
||||||
|
@Value("${stirling.resource.memory.high-threshold:0.75}")
|
||||||
|
private double memoryHighThreshold = 0.75; // 75% usage is high
|
||||||
|
|
||||||
|
@Value("${stirling.resource.cpu.critical-threshold:0.9}")
|
||||||
|
private double cpuCriticalThreshold = 0.9; // 90% usage is critical
|
||||||
|
|
||||||
|
@Value("${stirling.resource.cpu.high-threshold:0.75}")
|
||||||
|
private double cpuHighThreshold = 0.75; // 75% usage is high
|
||||||
|
|
||||||
|
@Value("${stirling.resource.monitor.interval-ms:60000}")
|
||||||
|
private long monitorIntervalMs = 60000; // 60 seconds
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
||||||
|
private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final AtomicReference<ResourceStatus> currentStatus =
|
||||||
|
new AtomicReference<>(ResourceStatus.OK);
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final AtomicReference<ResourceMetrics> latestMetrics =
|
||||||
|
new AtomicReference<>(new ResourceMetrics());
|
||||||
|
|
||||||
|
/** Represents the current status of system resources. */
|
||||||
|
public enum ResourceStatus {
|
||||||
|
/** Resources are available, normal operations can proceed */
|
||||||
|
OK,
|
||||||
|
|
||||||
|
/** Resources are under strain, consider queueing high-resource operations */
|
||||||
|
WARNING,
|
||||||
|
|
||||||
|
/** Resources are critically low, queue all operations */
|
||||||
|
CRITICAL
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detailed metrics about system resources. */
|
||||||
|
@Getter
|
||||||
|
public static class ResourceMetrics {
|
||||||
|
private final double cpuUsage;
|
||||||
|
private final double memoryUsage;
|
||||||
|
private final long freeMemoryBytes;
|
||||||
|
private final long totalMemoryBytes;
|
||||||
|
private final long maxMemoryBytes;
|
||||||
|
private final Instant timestamp;
|
||||||
|
|
||||||
|
public ResourceMetrics() {
|
||||||
|
this(0, 0, 0, 0, 0, Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceMetrics(
|
||||||
|
double cpuUsage,
|
||||||
|
double memoryUsage,
|
||||||
|
long freeMemoryBytes,
|
||||||
|
long totalMemoryBytes,
|
||||||
|
long maxMemoryBytes,
|
||||||
|
Instant timestamp) {
|
||||||
|
this.cpuUsage = cpuUsage;
|
||||||
|
this.memoryUsage = memoryUsage;
|
||||||
|
this.freeMemoryBytes = freeMemoryBytes;
|
||||||
|
this.totalMemoryBytes = totalMemoryBytes;
|
||||||
|
this.maxMemoryBytes = maxMemoryBytes;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the age of these metrics.
|
||||||
|
*
|
||||||
|
* @return Duration since these metrics were collected
|
||||||
|
*/
|
||||||
|
public Duration getAge() {
|
||||||
|
return Duration.between(timestamp, Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if these metrics are stale (older than threshold).
|
||||||
|
*
|
||||||
|
* @param thresholdMs Staleness threshold in milliseconds
|
||||||
|
* @return true if metrics are stale
|
||||||
|
*/
|
||||||
|
public boolean isStale(long thresholdMs) {
|
||||||
|
return getAge().toMillis() > thresholdMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initialize() {
|
||||||
|
log.debug("Starting resource monitoring with interval of {}ms", monitorIntervalMs);
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
this::updateResourceMetrics, 0, monitorIntervalMs, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
log.info("Shutting down resource monitoring");
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the resource metrics by sampling current system state. */
|
||||||
|
private void updateResourceMetrics() {
|
||||||
|
try {
|
||||||
|
// Get CPU usage
|
||||||
|
double cpuUsage = osMXBean.getSystemLoadAverage() / osMXBean.getAvailableProcessors();
|
||||||
|
if (cpuUsage < 0) cpuUsage = getAlternativeCpuLoad(); // Fallback if not available
|
||||||
|
|
||||||
|
// Get memory usage
|
||||||
|
long heapUsed = memoryMXBean.getHeapMemoryUsage().getUsed();
|
||||||
|
long nonHeapUsed = memoryMXBean.getNonHeapMemoryUsage().getUsed();
|
||||||
|
long totalUsed = heapUsed + nonHeapUsed;
|
||||||
|
|
||||||
|
long maxMemory = Runtime.getRuntime().maxMemory();
|
||||||
|
long totalMemory = Runtime.getRuntime().totalMemory();
|
||||||
|
long freeMemory = Runtime.getRuntime().freeMemory();
|
||||||
|
|
||||||
|
double memoryUsage = (double) totalUsed / maxMemory;
|
||||||
|
|
||||||
|
// Create new metrics
|
||||||
|
ResourceMetrics metrics =
|
||||||
|
new ResourceMetrics(
|
||||||
|
cpuUsage,
|
||||||
|
memoryUsage,
|
||||||
|
freeMemory,
|
||||||
|
totalMemory,
|
||||||
|
maxMemory,
|
||||||
|
Instant.now());
|
||||||
|
latestMetrics.set(metrics);
|
||||||
|
|
||||||
|
// Determine system status
|
||||||
|
ResourceStatus newStatus;
|
||||||
|
if (cpuUsage > cpuCriticalThreshold || memoryUsage > memoryCriticalThreshold) {
|
||||||
|
newStatus = ResourceStatus.CRITICAL;
|
||||||
|
} else if (cpuUsage > cpuHighThreshold || memoryUsage > memoryHighThreshold) {
|
||||||
|
newStatus = ResourceStatus.WARNING;
|
||||||
|
} else {
|
||||||
|
newStatus = ResourceStatus.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status if it changed
|
||||||
|
ResourceStatus oldStatus = currentStatus.getAndSet(newStatus);
|
||||||
|
if (oldStatus != newStatus) {
|
||||||
|
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
||||||
|
log.info(
|
||||||
|
"Current metrics - CPU: {:.1f}%, Memory: {:.1f}%, Free Memory: {} MB",
|
||||||
|
cpuUsage * 100, memoryUsage * 100, freeMemory / (1024 * 1024));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error updating resource metrics: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative method to estimate CPU load if getSystemLoadAverage() is not available. This is a
|
||||||
|
* fallback and less accurate than the official JMX method.
|
||||||
|
*
|
||||||
|
* @return Estimated CPU load as a value between 0.0 and 1.0
|
||||||
|
*/
|
||||||
|
private double getAlternativeCpuLoad() {
|
||||||
|
try {
|
||||||
|
// Try to get CPU time if available through reflection
|
||||||
|
// This is a fallback since we can't directly cast to platform-specific classes
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method m =
|
||||||
|
osMXBean.getClass().getDeclaredMethod("getProcessCpuLoad");
|
||||||
|
m.setAccessible(true);
|
||||||
|
return (double) m.invoke(osMXBean);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Try the older method
|
||||||
|
try {
|
||||||
|
java.lang.reflect.Method m =
|
||||||
|
osMXBean.getClass().getDeclaredMethod("getSystemCpuLoad");
|
||||||
|
m.setAccessible(true);
|
||||||
|
return (double) m.invoke(osMXBean);
|
||||||
|
} catch (Exception e2) {
|
||||||
|
log.trace(
|
||||||
|
"Could not get CPU load through reflection, assuming moderate load (0.5)");
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.trace("Could not get CPU load, assuming moderate load (0.5)");
|
||||||
|
return 0.5; // Default to moderate load
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the dynamic job queue capacity based on current resource usage.
|
||||||
|
*
|
||||||
|
* @param baseCapacity The base capacity when system is under minimal load
|
||||||
|
* @param minCapacity The minimum capacity to maintain even under high load
|
||||||
|
* @return The calculated job queue capacity
|
||||||
|
*/
|
||||||
|
public int calculateDynamicQueueCapacity(int baseCapacity, int minCapacity) {
|
||||||
|
ResourceMetrics metrics = latestMetrics.get();
|
||||||
|
ResourceStatus status = currentStatus.get();
|
||||||
|
|
||||||
|
// Simple linear reduction based on memory and CPU load
|
||||||
|
double capacityFactor =
|
||||||
|
switch (status) {
|
||||||
|
case OK -> 1.0;
|
||||||
|
case WARNING -> 0.6;
|
||||||
|
case CRITICAL -> 0.3;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply additional reduction based on specific memory pressure
|
||||||
|
if (metrics.memoryUsage > 0.8) {
|
||||||
|
capacityFactor *= 0.5; // Further reduce capacity under memory pressure
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate capacity with minimum safeguard
|
||||||
|
int capacity = (int) Math.max(minCapacity, Math.ceil(baseCapacity * capacityFactor));
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Dynamic queue capacity: {} (base: {}, factor: {:.2f}, status: {})",
|
||||||
|
capacity,
|
||||||
|
baseCapacity,
|
||||||
|
capacityFactor,
|
||||||
|
status);
|
||||||
|
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a job with the given weight can be executed immediately or should be queued based
|
||||||
|
* on current resource availability.
|
||||||
|
*
|
||||||
|
* @param resourceWeight The resource weight of the job (1-100)
|
||||||
|
* @return true if the job should be queued, false if it can run immediately
|
||||||
|
*/
|
||||||
|
public boolean shouldQueueJob(int resourceWeight) {
|
||||||
|
ResourceStatus status = currentStatus.get();
|
||||||
|
|
||||||
|
// Always run lightweight jobs (weight < 20) unless critical
|
||||||
|
if (resourceWeight < 20 && status != ResourceStatus.CRITICAL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Medium weight jobs run immediately if resources are OK
|
||||||
|
if (resourceWeight < 60 && status == ResourceStatus.OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heavy jobs (weight >= 60) and any job during WARNING/CRITICAL should be queued
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
|
||||||
|
/** Manages async tasks and their results */
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class TaskManager {
|
||||||
|
private final Map<String, JobResult> jobResults = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Value("${stirling.jobResultExpiryMinutes:30}")
|
||||||
|
private int jobResultExpiryMinutes = 30;
|
||||||
|
|
||||||
|
private final FileStorage fileStorage;
|
||||||
|
private final ScheduledExecutorService cleanupExecutor =
|
||||||
|
Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
|
/** Initialize the task manager and start the cleanup scheduler */
|
||||||
|
public TaskManager(FileStorage fileStorage) {
|
||||||
|
this.fileStorage = fileStorage;
|
||||||
|
|
||||||
|
// Schedule periodic cleanup of old job results
|
||||||
|
cleanupExecutor.scheduleAtFixedRate(
|
||||||
|
this::cleanupOldJobs,
|
||||||
|
10, // Initial delay
|
||||||
|
10, // Interval
|
||||||
|
TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Task manager initialized with job result expiry of {} minutes",
|
||||||
|
jobResultExpiryMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 statistics about all jobs in the system
|
||||||
|
*
|
||||||
|
* @return Job statistics
|
||||||
|
*/
|
||||||
|
public JobStats getJobStats() {
|
||||||
|
int totalJobs = jobResults.size();
|
||||||
|
int activeJobs = 0;
|
||||||
|
int completedJobs = 0;
|
||||||
|
int failedJobs = 0;
|
||||||
|
int successfulJobs = 0;
|
||||||
|
int fileResultJobs = 0;
|
||||||
|
|
||||||
|
LocalDateTime oldestActiveJobTime = null;
|
||||||
|
LocalDateTime newestActiveJobTime = null;
|
||||||
|
long totalProcessingTimeMs = 0;
|
||||||
|
|
||||||
|
for (JobResult result : jobResults.values()) {
|
||||||
|
if (result.isComplete()) {
|
||||||
|
completedJobs++;
|
||||||
|
|
||||||
|
// Calculate processing time for completed jobs
|
||||||
|
if (result.getCreatedAt() != null && result.getCompletedAt() != null) {
|
||||||
|
long processingTimeMs =
|
||||||
|
java.time.Duration.between(
|
||||||
|
result.getCreatedAt(), result.getCompletedAt())
|
||||||
|
.toMillis();
|
||||||
|
totalProcessingTimeMs += processingTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getError() != null) {
|
||||||
|
failedJobs++;
|
||||||
|
} else {
|
||||||
|
successfulJobs++;
|
||||||
|
if (result.getFileId() != null) {
|
||||||
|
fileResultJobs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeJobs++;
|
||||||
|
|
||||||
|
// Track oldest and newest active jobs
|
||||||
|
if (result.getCreatedAt() != null) {
|
||||||
|
if (oldestActiveJobTime == null
|
||||||
|
|| result.getCreatedAt().isBefore(oldestActiveJobTime)) {
|
||||||
|
oldestActiveJobTime = result.getCreatedAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newestActiveJobTime == null
|
||||||
|
|| result.getCreatedAt().isAfter(newestActiveJobTime)) {
|
||||||
|
newestActiveJobTime = result.getCreatedAt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate average processing time
|
||||||
|
long averageProcessingTimeMs =
|
||||||
|
completedJobs > 0 ? totalProcessingTimeMs / completedJobs : 0;
|
||||||
|
|
||||||
|
return JobStats.builder()
|
||||||
|
.totalJobs(totalJobs)
|
||||||
|
.activeJobs(activeJobs)
|
||||||
|
.completedJobs(completedJobs)
|
||||||
|
.failedJobs(failedJobs)
|
||||||
|
.successfulJobs(successfulJobs)
|
||||||
|
.fileResultJobs(fileResultJobs)
|
||||||
|
.oldestActiveJobTime(oldestActiveJobTime)
|
||||||
|
.newestActiveJobTime(newestActiveJobTime)
|
||||||
|
.averageProcessingTimeMs(averageProcessingTimeMs)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clean up old completed job results */
|
||||||
|
public void cleanupOldJobs() {
|
||||||
|
LocalDateTime expiryThreshold =
|
||||||
|
LocalDateTime.now().minus(jobResultExpiryMinutes, ChronoUnit.MINUTES);
|
||||||
|
int removedCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (Map.Entry<String, JobResult> entry : jobResults.entrySet()) {
|
||||||
|
JobResult result = entry.getValue();
|
||||||
|
|
||||||
|
// Remove completed jobs that are older than the expiry threshold
|
||||||
|
if (result.isComplete()
|
||||||
|
&& result.getCompletedAt() != null
|
||||||
|
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
||||||
|
|
||||||
|
// If the job has a file result, delete the file
|
||||||
|
if (result.getFileId() != null) {
|
||||||
|
try {
|
||||||
|
fileStorage.deleteFile(result.getFileId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to delete file for job {}: {}",
|
||||||
|
entry.getKey(),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the job result
|
||||||
|
jobResults.remove(entry.getKey());
|
||||||
|
removedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0) {
|
||||||
|
log.info("Cleaned up {} expired job results", removedCount);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during job cleanup: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shutdown the cleanup executor */
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
try {
|
||||||
|
log.info("Shutting down job result cleanup executor");
|
||||||
|
cleanupExecutor.shutdown();
|
||||||
|
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||||
|
cleanupExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
cleanupExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ExecutorFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an ExecutorService using virtual threads if available (Java 21+), or falls back to a
|
||||||
|
* cached thread pool on older Java versions.
|
||||||
|
*/
|
||||||
|
public static ExecutorService newVirtualOrCachedThreadExecutor() {
|
||||||
|
try {
|
||||||
|
ExecutorService executor =
|
||||||
|
(ExecutorService)
|
||||||
|
Executors.class
|
||||||
|
.getMethod("newVirtualThreadPerTaskExecutor")
|
||||||
|
.invoke(null);
|
||||||
|
return executor;
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
log.debug("Virtual threads not available; falling back to cached thread pool.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Error initializing virtual thread executor: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Executors.newCachedThreadPool();
|
||||||
|
}
|
||||||
|
}
|
||||||
+214
@@ -0,0 +1,214 @@
|
|||||||
|
package stirling.software.common.annotations;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import stirling.software.common.aop.AutoJobAspect;
|
||||||
|
import stirling.software.common.controller.WebSocketProgressController;
|
||||||
|
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;
|
||||||
|
import stirling.software.common.service.JobQueue;
|
||||||
|
import stirling.software.common.service.ResourceMonitor;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class AutoJobPostMappingIntegrationTest {
|
||||||
|
|
||||||
|
private AutoJobAspect autoJobAspect;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JobExecutorService jobExecutorService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileOrUploadService fileOrUploadService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private WebSocketProgressController webSocketSender;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ResourceMonitor resourceMonitor;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JobQueue jobQueue;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
autoJobAspect = new AutoJobAspect(
|
||||||
|
jobExecutorService,
|
||||||
|
request,
|
||||||
|
fileOrUploadService,
|
||||||
|
fileStorage,
|
||||||
|
webSocketSender
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ProceedingJoinPoint joinPoint;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AutoJobPostMapping autoJobPostMapping;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Supplier<Object>> workCaptor;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Boolean> asyncCaptor;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Long> timeoutCaptor;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Boolean> queueableCaptor;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Integer> resourceWeightCaptor;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExecuteWithCustomParameters() throws Throwable {
|
||||||
|
// Given
|
||||||
|
PDFFile pdfFile = new PDFFile();
|
||||||
|
pdfFile.setFileId("test-file-id");
|
||||||
|
Object[] args = new Object[] { pdfFile };
|
||||||
|
|
||||||
|
when(joinPoint.getArgs()).thenReturn(args);
|
||||||
|
when(request.getParameter("async")).thenReturn("true");
|
||||||
|
when(autoJobPostMapping.timeout()).thenReturn(60000L);
|
||||||
|
when(autoJobPostMapping.retryCount()).thenReturn(3);
|
||||||
|
when(autoJobPostMapping.trackProgress()).thenReturn(true);
|
||||||
|
when(autoJobPostMapping.queueable()).thenReturn(true);
|
||||||
|
when(autoJobPostMapping.resourceWeight()).thenReturn(75);
|
||||||
|
|
||||||
|
MultipartFile mockFile = mock(MultipartFile.class);
|
||||||
|
when(fileStorage.retrieveFile("test-file-id")).thenReturn(mockFile);
|
||||||
|
|
||||||
|
|
||||||
|
when(jobExecutorService.runJobGeneric(
|
||||||
|
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||||
|
.thenReturn(ResponseEntity.ok("success"));
|
||||||
|
|
||||||
|
// When
|
||||||
|
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(ResponseEntity.ok("success"), result);
|
||||||
|
|
||||||
|
verify(jobExecutorService).runJobGeneric(
|
||||||
|
asyncCaptor.capture(),
|
||||||
|
workCaptor.capture(),
|
||||||
|
timeoutCaptor.capture(),
|
||||||
|
queueableCaptor.capture(),
|
||||||
|
resourceWeightCaptor.capture());
|
||||||
|
|
||||||
|
assertTrue(asyncCaptor.getValue(), "Async should be true");
|
||||||
|
assertEquals(60000L, timeoutCaptor.getValue(), "Timeout should be 60000ms");
|
||||||
|
assertTrue(queueableCaptor.getValue(), "Queueable should be true");
|
||||||
|
assertEquals(75, resourceWeightCaptor.getValue(), "Resource weight should be 75");
|
||||||
|
|
||||||
|
// Test that file was resolved
|
||||||
|
assertNotNull(pdfFile.getFileInput(), "File input should be set");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRetryOnError() throws Throwable {
|
||||||
|
// Given
|
||||||
|
when(joinPoint.getArgs()).thenReturn(new Object[0]);
|
||||||
|
when(request.getParameter("async")).thenReturn("false");
|
||||||
|
when(autoJobPostMapping.timeout()).thenReturn(-1L);
|
||||||
|
when(autoJobPostMapping.retryCount()).thenReturn(2);
|
||||||
|
when(autoJobPostMapping.trackProgress()).thenReturn(false);
|
||||||
|
when(autoJobPostMapping.queueable()).thenReturn(false);
|
||||||
|
when(autoJobPostMapping.resourceWeight()).thenReturn(50);
|
||||||
|
|
||||||
|
// First call throws exception, second succeeds
|
||||||
|
when(joinPoint.proceed(any()))
|
||||||
|
.thenThrow(new RuntimeException("First attempt failed"))
|
||||||
|
.thenReturn(ResponseEntity.ok("retry succeeded"));
|
||||||
|
|
||||||
|
// Mock jobExecutorService to execute the work immediately
|
||||||
|
when(jobExecutorService.runJobGeneric(
|
||||||
|
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Supplier<Object> work = invocation.getArgument(1);
|
||||||
|
return work.get();
|
||||||
|
});
|
||||||
|
|
||||||
|
// When
|
||||||
|
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(ResponseEntity.ok("retry succeeded"), result);
|
||||||
|
|
||||||
|
// Verify that proceed was called twice (initial attempt + 1 retry)
|
||||||
|
verify(joinPoint, times(2)).proceed(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandlePDFFileWithAsyncRequests() throws Throwable {
|
||||||
|
// Given
|
||||||
|
PDFFile pdfFile = new PDFFile();
|
||||||
|
pdfFile.setFileInput(mock(MultipartFile.class));
|
||||||
|
Object[] args = new Object[] { pdfFile };
|
||||||
|
|
||||||
|
when(joinPoint.getArgs()).thenReturn(args);
|
||||||
|
when(request.getParameter("async")).thenReturn("true");
|
||||||
|
when(autoJobPostMapping.retryCount()).thenReturn(1);
|
||||||
|
|
||||||
|
when(fileStorage.storeFile(any(MultipartFile.class))).thenReturn("stored-file-id");
|
||||||
|
when(fileStorage.retrieveFile("stored-file-id")).thenReturn(mock(MultipartFile.class));
|
||||||
|
|
||||||
|
// Mock job executor to return a successful response
|
||||||
|
when(jobExecutorService.runJobGeneric(
|
||||||
|
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||||
|
.thenReturn(ResponseEntity.ok("success"));
|
||||||
|
|
||||||
|
// When
|
||||||
|
autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals("stored-file-id", pdfFile.getFileId(),
|
||||||
|
"FileId should be set to the stored file id");
|
||||||
|
assertNotNull(pdfFile.getFileInput(), "FileInput should be replaced with persistent file");
|
||||||
|
|
||||||
|
// Verify storage operations
|
||||||
|
verify(fileStorage).storeFile(any(MultipartFile.class));
|
||||||
|
verify(fileStorage).retrieveFile("stored-file-id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move PDFFileTest to its own class file if needed
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package stirling.software.common.controller;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
import stirling.software.common.service.FileStorage;
|
||||||
|
import stirling.software.common.service.TaskManager;
|
||||||
|
|
||||||
|
class JobControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TaskManager taskManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private JobController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStatus_ExistingJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobStatus(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(mockResult, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStatus_NonExistentJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "non-existent-job";
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobStatus(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_CompletedSuccessfulWithObject() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.setComplete(true);
|
||||||
|
String resultObject = "Test result";
|
||||||
|
mockResult.completeWithResult(resultObject);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(resultObject, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_CompletedSuccessfulWithFile() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
String fileId = "file-id";
|
||||||
|
String originalFileName = "test.pdf";
|
||||||
|
String contentType = "application/pdf";
|
||||||
|
byte[] fileContent = "Test file content".getBytes();
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
when(fileStorage.retrieveBytes(fileId)).thenReturn(fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(contentType, response.getHeaders().getFirst("Content-Type"));
|
||||||
|
assertTrue(response.getHeaders().getFirst("Content-Disposition").contains(originalFileName));
|
||||||
|
assertEquals(fileContent, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_CompletedWithError() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
String errorMessage = "Test error";
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.failWithError(errorMessage);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody().toString().contains(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_IncompleteJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.setComplete(false);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody().toString().contains("not complete"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_NonExistentJob() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "non-existent-job";
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobResult_ErrorRetrievingFile() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
String fileId = "file-id";
|
||||||
|
String originalFileName = "test.pdf";
|
||||||
|
String contentType = "application/pdf";
|
||||||
|
|
||||||
|
JobResult mockResult = new JobResult();
|
||||||
|
mockResult.setJobId(jobId);
|
||||||
|
mockResult.completeWithFile(fileId, originalFileName, contentType);
|
||||||
|
|
||||||
|
when(taskManager.getJobResult(jobId)).thenReturn(mockResult);
|
||||||
|
when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.getJobResult(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody().toString().contains("Error retrieving file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStats() {
|
||||||
|
// Arrange
|
||||||
|
JobStats mockStats = JobStats.builder()
|
||||||
|
.totalJobs(10)
|
||||||
|
.activeJobs(3)
|
||||||
|
.completedJobs(7)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(taskManager.getJobStats()).thenReturn(mockStats);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<JobStats> response = controller.getJobStats();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals(mockStats, response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanupOldJobs() {
|
||||||
|
// Arrange
|
||||||
|
when(taskManager.getJobStats())
|
||||||
|
.thenReturn(JobStats.builder().totalJobs(10).build())
|
||||||
|
.thenReturn(JobStats.builder().totalJobs(7).build());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ResponseEntity<?> response = controller.cleanupOldJobs();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> responseBody = (Map<String, Object>) response.getBody();
|
||||||
|
assertEquals("Cleanup complete", responseBody.get("message"));
|
||||||
|
assertEquals(3, responseBody.get("removedJobs"));
|
||||||
|
assertEquals(7, responseBody.get("remainingJobs"));
|
||||||
|
|
||||||
|
verify(taskManager).cleanupOldJobs();
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
package stirling.software.common.controller;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobProgress;
|
||||||
|
|
||||||
|
class WebSocketProgressControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SimpMessagingTemplate messagingTemplate;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private WebSocketProgressController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendProgress_WithMessagingTemplate() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
controller.sendProgress(jobId, progress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(messagingTemplate).convertAndSend("/topic/progress/" + jobId, progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendProgress_WithNullMessagingTemplate() {
|
||||||
|
// Arrange
|
||||||
|
WebSocketProgressController controllerWithNullTemplate = new WebSocketProgressController();
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||||
|
|
||||||
|
// Act - should not throw exception even with null template
|
||||||
|
controllerWithNullTemplate.sendProgress(jobId, progress);
|
||||||
|
|
||||||
|
// No assertion needed - test passes if no exception is thrown
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetMessagingTemplate() {
|
||||||
|
// Arrange
|
||||||
|
WebSocketProgressController newController = new WebSocketProgressController();
|
||||||
|
SimpMessagingTemplate newTemplate = mock(SimpMessagingTemplate.class);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
newController.setMessagingTemplate(newTemplate);
|
||||||
|
String jobId = "test-job-id";
|
||||||
|
JobProgress progress = new JobProgress(jobId, "In Progress", 50, "Processing");
|
||||||
|
newController.sendProgress(jobId, progress);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(newTemplate).convertAndSend("/topic/progress/" + jobId, progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.mockito.AdditionalAnswers.*;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
class FileStorageTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileOrUploadService fileOrUploadService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
private MultipartFile mockFile;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
ReflectionTestUtils.setField(fileStorage, "tempDirPath", tempDir.toString());
|
||||||
|
|
||||||
|
// Create a mock MultipartFile
|
||||||
|
mockFile = mock(MultipartFile.class);
|
||||||
|
when(mockFile.getOriginalFilename()).thenReturn("test.pdf");
|
||||||
|
when(mockFile.getContentType()).thenReturn("application/pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStoreFile() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
when(mockFile.getBytes()).thenReturn(fileContent);
|
||||||
|
|
||||||
|
// Set up mock to handle transferTo by writing the file
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
java.io.File file = invocation.getArgument(0);
|
||||||
|
Files.write(file.toPath(), fileContent);
|
||||||
|
return null;
|
||||||
|
}).when(mockFile).transferTo(any(java.io.File.class));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String fileId = fileStorage.storeFile(mockFile);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(fileId);
|
||||||
|
assertTrue(Files.exists(tempDir.resolve(fileId)));
|
||||||
|
verify(mockFile).transferTo(any(java.io.File.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStoreBytes() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String originalName = "test.pdf";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String fileId = fileStorage.storeBytes(fileContent, originalName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(fileId);
|
||||||
|
assertTrue(Files.exists(tempDir.resolve(fileId)));
|
||||||
|
assertArrayEquals(fileContent, Files.readAllBytes(tempDir.resolve(fileId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveFile() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
MultipartFile expectedFile = mock(MultipartFile.class);
|
||||||
|
when(fileOrUploadService.toMockMultipartFile(eq(fileId), eq(fileContent)))
|
||||||
|
.thenReturn(expectedFile);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
MultipartFile result = fileStorage.retrieveFile(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertSame(expectedFile, result);
|
||||||
|
verify(fileOrUploadService).toMockMultipartFile(eq(fileId), eq(fileContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveBytes() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
byte[] result = fileStorage.retrieveBytes(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertArrayEquals(fileContent, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveFile_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertThrows(IOException.class, () -> fileStorage.retrieveFile(nonExistentFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRetrieveBytes_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertThrows(IOException.class, () -> fileStorage.retrieveBytes(nonExistentFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteFile() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.deleteFile(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result);
|
||||||
|
assertFalse(Files.exists(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteFile_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.deleteFile(nonExistentFileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertFalse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFileExists() throws IOException {
|
||||||
|
// Arrange
|
||||||
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
|
String fileId = UUID.randomUUID().toString();
|
||||||
|
Path filePath = tempDir.resolve(fileId);
|
||||||
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.fileExists(fileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFileExists_FileNotFound() {
|
||||||
|
// Arrange
|
||||||
|
String nonExistentFileId = "non-existent-file";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
boolean result = fileStorage.fileExists(nonExistentFileId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertFalse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import stirling.software.common.controller.WebSocketProgressController;
|
||||||
|
import stirling.software.common.model.job.JobProgress;
|
||||||
|
import stirling.software.common.model.job.JobResponse;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class JobExecutorServiceTest {
|
||||||
|
|
||||||
|
private JobExecutorService jobExecutorService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TaskManager taskManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private WebSocketProgressController webSocketSender;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ResourceMonitor resourceMonitor;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JobQueue jobQueue;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<String> jobIdCaptor;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// Initialize the service manually with all its dependencies
|
||||||
|
jobExecutorService = new JobExecutorService(
|
||||||
|
taskManager,
|
||||||
|
webSocketSender,
|
||||||
|
fileStorage,
|
||||||
|
request,
|
||||||
|
resourceMonitor,
|
||||||
|
jobQueue,
|
||||||
|
30000L, // asyncRequestTimeoutMs
|
||||||
|
"30m" // sessionTimeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRunSyncJobSuccessfully() throws Exception {
|
||||||
|
// Given
|
||||||
|
Supplier<Object> work = () -> "test-result";
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertEquals("test-result", response.getBody());
|
||||||
|
|
||||||
|
// Verify request attribute was set with jobId
|
||||||
|
verify(request).setAttribute(eq("jobId"), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRunAsyncJobSuccessfully() throws Exception {
|
||||||
|
// Given
|
||||||
|
Supplier<Object> work = () -> "test-result";
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResponseEntity<?> response = jobExecutorService.runJobGeneric(true, work);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody() instanceof JobResponse);
|
||||||
|
JobResponse<?> jobResponse = (JobResponse<?>) response.getBody();
|
||||||
|
assertTrue(jobResponse.isAsync());
|
||||||
|
assertNotNull(jobResponse.getJobId());
|
||||||
|
|
||||||
|
// Verify task manager was called
|
||||||
|
verify(taskManager).createTask(jobIdCaptor.capture());
|
||||||
|
verify(webSocketSender, Mockito.atLeastOnce()).sendProgress(eq(jobIdCaptor.getValue()), any(JobProgress.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleSyncJobError() {
|
||||||
|
// Given
|
||||||
|
Supplier<Object> work = () -> {
|
||||||
|
throw new RuntimeException("Test error");
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, String> errorMap = (Map<String, String>) response.getBody();
|
||||||
|
assertEquals("Job failed: Test error", errorMap.get("error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldQueueJobWhenResourcesLimited() {
|
||||||
|
// Given
|
||||||
|
Supplier<Object> work = () -> "test-result";
|
||||||
|
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
|
||||||
|
|
||||||
|
// Configure resourceMonitor to indicate job should be queued
|
||||||
|
when(resourceMonitor.shouldQueueJob(80)).thenReturn(true);
|
||||||
|
|
||||||
|
// Configure jobQueue to return our future
|
||||||
|
when(jobQueue.queueJob(anyString(), eq(80), any(), anyLong())).thenReturn(future);
|
||||||
|
|
||||||
|
// When
|
||||||
|
ResponseEntity<?> response = jobExecutorService.runJobGeneric(
|
||||||
|
true, work, 5000, true, 80);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertTrue(response.getBody() instanceof JobResponse);
|
||||||
|
|
||||||
|
// Verify job was queued
|
||||||
|
verify(jobQueue).queueJob(anyString(), eq(80), any(), eq(5000L));
|
||||||
|
verify(taskManager).createTask(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseCustomTimeoutWhenProvided() throws Exception {
|
||||||
|
// Given
|
||||||
|
Supplier<Object> work = () -> "test-result";
|
||||||
|
long customTimeout = 60000L;
|
||||||
|
|
||||||
|
// Use reflection to access the private executeWithTimeout method
|
||||||
|
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
||||||
|
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
||||||
|
executeMethod.setAccessible(true);
|
||||||
|
|
||||||
|
// Create a spy on the JobExecutorService to verify method calls
|
||||||
|
JobExecutorService spy = Mockito.spy(jobExecutorService);
|
||||||
|
|
||||||
|
// When
|
||||||
|
spy.runJobGeneric(false, work, customTimeout);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify(spy).runJobGeneric(eq(false), any(Supplier.class), eq(customTimeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldHandleTimeout() throws Exception {
|
||||||
|
// Given
|
||||||
|
Supplier<Object> work = () -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100); // Simulate long-running job
|
||||||
|
return "test-result";
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use reflection to access the private executeWithTimeout method
|
||||||
|
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
||||||
|
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
||||||
|
executeMethod.setAccessible(true);
|
||||||
|
|
||||||
|
// When/Then
|
||||||
|
try {
|
||||||
|
executeMethod.invoke(jobExecutorService, work, 1L); // Very short timeout
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertTrue(e.getCause() instanceof TimeoutException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import stirling.software.common.controller.WebSocketProgressController;
|
||||||
|
import stirling.software.common.model.job.JobProgress;
|
||||||
|
import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class JobQueueTest {
|
||||||
|
|
||||||
|
private JobQueue jobQueue;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ResourceMonitor resourceMonitor;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private WebSocketProgressController webSocketSender;
|
||||||
|
|
||||||
|
private final AtomicReference<ResourceStatus> statusRef = new AtomicReference<>(ResourceStatus.OK);
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// Mark stubbing as lenient to avoid UnnecessaryStubbingException
|
||||||
|
lenient().when(resourceMonitor.calculateDynamicQueueCapacity(anyInt(), anyInt())).thenReturn(10);
|
||||||
|
lenient().when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
||||||
|
|
||||||
|
jobQueue = new JobQueue(resourceMonitor, webSocketSender);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldQueueJob() {
|
||||||
|
String jobId = "test-job-1";
|
||||||
|
int resourceWeight = 50;
|
||||||
|
Supplier<Object> work = () -> "test-result";
|
||||||
|
long timeoutMs = 1000;
|
||||||
|
|
||||||
|
jobQueue.queueJob(jobId, resourceWeight, work, timeoutMs);
|
||||||
|
|
||||||
|
verify(webSocketSender).sendProgress(
|
||||||
|
org.mockito.ArgumentMatchers.eq(jobId),
|
||||||
|
org.mockito.ArgumentMatchers.any(JobProgress.class));
|
||||||
|
|
||||||
|
assertTrue(jobQueue.isJobQueued(jobId));
|
||||||
|
assertEquals(1, jobQueue.getTotalQueuedJobs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCancelJob() {
|
||||||
|
String jobId = "test-job-2";
|
||||||
|
Supplier<Object> work = () -> "test-result";
|
||||||
|
|
||||||
|
jobQueue.queueJob(jobId, 50, work, 1000);
|
||||||
|
boolean cancelled = jobQueue.cancelJob(jobId);
|
||||||
|
|
||||||
|
assertTrue(cancelled);
|
||||||
|
assertFalse(jobQueue.isJobQueued(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetQueueStats() {
|
||||||
|
when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
||||||
|
|
||||||
|
jobQueue.queueJob("job1", 50, () -> "ok", 1000);
|
||||||
|
jobQueue.queueJob("job2", 50, () -> "ok", 1000);
|
||||||
|
jobQueue.cancelJob("job2");
|
||||||
|
|
||||||
|
Map<String, Object> stats = jobQueue.getQueueStats();
|
||||||
|
|
||||||
|
assertEquals(2, stats.get("totalQueuedJobs"));
|
||||||
|
assertTrue(stats.containsKey("queuedJobs"));
|
||||||
|
assertTrue(stats.containsKey("resourceStatus"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCalculateQueueCapacity() {
|
||||||
|
when(resourceMonitor.calculateDynamicQueueCapacity(5, 2)).thenReturn(8);
|
||||||
|
int capacity = resourceMonitor.calculateDynamicQueueCapacity(5, 2);
|
||||||
|
assertEquals(8, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCheckIfJobIsQueued() {
|
||||||
|
String jobId = "job-123";
|
||||||
|
Supplier<Object> work = () -> "hello";
|
||||||
|
|
||||||
|
jobQueue.queueJob(jobId, 40, work, 500);
|
||||||
|
|
||||||
|
assertTrue(jobQueue.isJobQueued(jobId));
|
||||||
|
assertFalse(jobQueue.isJobQueued("nonexistent"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.lang.management.MemoryMXBean;
|
||||||
|
import java.lang.management.MemoryUsage;
|
||||||
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Spy;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import stirling.software.common.service.ResourceMonitor.ResourceMetrics;
|
||||||
|
import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ResourceMonitorTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ResourceMonitor resourceMonitor;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OperatingSystemMXBean osMXBean;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private MemoryMXBean memoryMXBean;
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private AtomicReference<ResourceStatus> currentStatus = new AtomicReference<>(ResourceStatus.OK);
|
||||||
|
|
||||||
|
@Spy
|
||||||
|
private AtomicReference<ResourceMetrics> latestMetrics = new AtomicReference<>(new ResourceMetrics());
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// Set thresholds for testing
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "memoryCriticalThreshold", 0.9);
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "memoryHighThreshold", 0.75);
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "cpuCriticalThreshold", 0.9);
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "cpuHighThreshold", 0.75);
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "osMXBean", osMXBean);
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "memoryMXBean", memoryMXBean);
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "currentStatus", currentStatus);
|
||||||
|
ReflectionTestUtils.setField(resourceMonitor, "latestMetrics", latestMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCalculateDynamicQueueCapacity() {
|
||||||
|
// Given
|
||||||
|
int baseCapacity = 10;
|
||||||
|
int minCapacity = 2;
|
||||||
|
|
||||||
|
// Mock current status as OK
|
||||||
|
currentStatus.set(ResourceStatus.OK);
|
||||||
|
|
||||||
|
// When
|
||||||
|
int capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(baseCapacity, capacity, "With OK status, capacity should equal base capacity");
|
||||||
|
|
||||||
|
// Given
|
||||||
|
currentStatus.set(ResourceStatus.WARNING);
|
||||||
|
|
||||||
|
// When
|
||||||
|
capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(6, capacity, "With WARNING status, capacity should be reduced to 60%");
|
||||||
|
|
||||||
|
// Given
|
||||||
|
currentStatus.set(ResourceStatus.CRITICAL);
|
||||||
|
|
||||||
|
// When
|
||||||
|
capacity = resourceMonitor.calculateDynamicQueueCapacity(baseCapacity, minCapacity);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(3, capacity, "With CRITICAL status, capacity should be reduced to 30%");
|
||||||
|
|
||||||
|
// Test minimum capacity enforcement
|
||||||
|
assertEquals(minCapacity, resourceMonitor.calculateDynamicQueueCapacity(1, minCapacity),
|
||||||
|
"Should never go below minimum capacity");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({
|
||||||
|
"10, OK, false", // Light job, OK status
|
||||||
|
"10, WARNING, false", // Light job, WARNING status
|
||||||
|
"10, CRITICAL, true", // Light job, CRITICAL status
|
||||||
|
"30, OK, false", // Medium job, OK status
|
||||||
|
"30, WARNING, true", // Medium job, WARNING status
|
||||||
|
"30, CRITICAL, true", // Medium job, CRITICAL status
|
||||||
|
"80, OK, true", // Heavy job, OK status
|
||||||
|
"80, WARNING, true", // Heavy job, WARNING status
|
||||||
|
"80, CRITICAL, true" // Heavy job, CRITICAL status
|
||||||
|
})
|
||||||
|
void shouldQueueJobBasedOnWeightAndStatus(int weight, ResourceStatus status, boolean shouldQueue) {
|
||||||
|
// Given
|
||||||
|
currentStatus.set(status);
|
||||||
|
|
||||||
|
// When
|
||||||
|
boolean result = resourceMonitor.shouldQueueJob(weight);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(shouldQueue, result,
|
||||||
|
String.format("For weight %d and status %s, shouldQueue should be %s",
|
||||||
|
weight, status, shouldQueue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resourceMetricsShouldDetectStaleState() {
|
||||||
|
// Given
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant pastInstant = now.minusMillis(6000);
|
||||||
|
|
||||||
|
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
||||||
|
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
||||||
|
|
||||||
|
// When/Then
|
||||||
|
assertTrue(staleMetrics.isStale(5000), "Metrics from 6 seconds ago should be stale with 5s threshold");
|
||||||
|
assertFalse(freshMetrics.isStale(5000), "Fresh metrics should not be stale");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
|
||||||
|
class TaskManagerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FileStorage fileStorage;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private TaskManager taskManager;
|
||||||
|
|
||||||
|
private AutoCloseable closeable;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
closeable = MockitoAnnotations.openMocks(this);
|
||||||
|
ReflectionTestUtils.setField(taskManager, "jobResultExpiryMinutes", 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
closeable.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateTask() {
|
||||||
|
// Act
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(jobId, result.getJobId());
|
||||||
|
assertFalse(result.isComplete());
|
||||||
|
assertNotNull(result.getCreatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
Object resultObject = "Test result";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setResult(jobId, resultObject);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(resultObject, result.getResult());
|
||||||
|
assertNotNull(result.getCompletedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetFileResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
String fileId = "file-id";
|
||||||
|
String originalFileName = "test.pdf";
|
||||||
|
String contentType = "application/pdf";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(fileId, result.getFileId());
|
||||||
|
assertEquals(originalFileName, result.getOriginalFileName());
|
||||||
|
assertEquals(contentType, result.getContentType());
|
||||||
|
assertNotNull(result.getCompletedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetError() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
String errorMessage = "Test error";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setError(jobId, errorMessage);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(errorMessage, result.getError());
|
||||||
|
assertNotNull(result.getCompletedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetComplete_WithExistingResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
Object resultObject = "Test result";
|
||||||
|
taskManager.setResult(jobId, resultObject);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setComplete(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals(resultObject, result.getResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetComplete_WithoutExistingResult() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.setComplete(jobId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isComplete());
|
||||||
|
assertEquals("Task completed successfully", result.getResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsComplete() {
|
||||||
|
// Arrange
|
||||||
|
String jobId = UUID.randomUUID().toString();
|
||||||
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
|
// Assert - not complete initially
|
||||||
|
assertFalse(taskManager.isComplete(jobId));
|
||||||
|
|
||||||
|
// Act - mark as complete
|
||||||
|
taskManager.setComplete(jobId);
|
||||||
|
|
||||||
|
// Assert - now complete
|
||||||
|
assertTrue(taskManager.isComplete(jobId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetJobStats() {
|
||||||
|
// Arrange
|
||||||
|
// 1. Create active job
|
||||||
|
String activeJobId = "active-job";
|
||||||
|
taskManager.createTask(activeJobId);
|
||||||
|
|
||||||
|
// 2. Create completed successful job with file
|
||||||
|
String successFileJobId = "success-file-job";
|
||||||
|
taskManager.createTask(successFileJobId);
|
||||||
|
taskManager.setFileResult(successFileJobId, "file-id", "test.pdf", "application/pdf");
|
||||||
|
|
||||||
|
// 3. Create completed successful job without file
|
||||||
|
String successJobId = "success-job";
|
||||||
|
taskManager.createTask(successJobId);
|
||||||
|
taskManager.setResult(successJobId, "Result");
|
||||||
|
|
||||||
|
// 4. Create failed job
|
||||||
|
String failedJobId = "failed-job";
|
||||||
|
taskManager.createTask(failedJobId);
|
||||||
|
taskManager.setError(failedJobId, "Error message");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
JobStats stats = taskManager.getJobStats();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(4, stats.getTotalJobs());
|
||||||
|
assertEquals(1, stats.getActiveJobs());
|
||||||
|
assertEquals(3, stats.getCompletedJobs());
|
||||||
|
assertEquals(1, stats.getFailedJobs());
|
||||||
|
assertEquals(2, stats.getSuccessfulJobs());
|
||||||
|
assertEquals(1, stats.getFileResultJobs());
|
||||||
|
assertNotNull(stats.getNewestActiveJobTime());
|
||||||
|
assertNotNull(stats.getOldestActiveJobTime());
|
||||||
|
assertTrue(stats.getAverageProcessingTimeMs() >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCleanupOldJobs() throws Exception {
|
||||||
|
// Arrange
|
||||||
|
// 1. Create a recent completed job
|
||||||
|
String recentJobId = "recent-job";
|
||||||
|
taskManager.createTask(recentJobId);
|
||||||
|
taskManager.setResult(recentJobId, "Result");
|
||||||
|
|
||||||
|
// 2. Create an old completed job with file result
|
||||||
|
String oldJobId = "old-job";
|
||||||
|
taskManager.createTask(oldJobId);
|
||||||
|
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
||||||
|
|
||||||
|
// Manually set the completion time to be older than the expiry
|
||||||
|
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
||||||
|
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||||
|
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||||
|
ReflectionTestUtils.setField(oldJob, "fileId", "file-id");
|
||||||
|
ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf");
|
||||||
|
ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf");
|
||||||
|
|
||||||
|
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
||||||
|
|
||||||
|
// Obtain access to the private jobResults map
|
||||||
|
Map<String, JobResult> jobResultsMap = (Map<String, JobResult>) ReflectionTestUtils.getField(taskManager, "jobResults");
|
||||||
|
|
||||||
|
// 3. Create an active job
|
||||||
|
String activeJobId = "active-job";
|
||||||
|
taskManager.createTask(activeJobId);
|
||||||
|
|
||||||
|
// Verify all jobs are in the map
|
||||||
|
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(oldJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
taskManager.cleanupOldJobs();
|
||||||
|
|
||||||
|
// Assert - the old job should be removed
|
||||||
|
assertFalse(jobResultsMap.containsKey(oldJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||||
|
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||||
|
verify(fileStorage).deleteFile("file-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShutdown() throws Exception {
|
||||||
|
// This mainly tests that the shutdown method doesn't throw exceptions
|
||||||
|
taskManager.shutdown();
|
||||||
|
|
||||||
|
// Verify the executor service is shutdown
|
||||||
|
// This is difficult to test directly, but we can verify it doesn't throw exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# AutoJobPostMapping Annotation
|
||||||
|
|
||||||
|
The `AutoJobPostMapping` annotation simplifies the creation of job-based REST endpoints in Stirling-PDF. It automatically handles job execution, file persistence, error handling, retries, and progress tracking.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Wraps endpoint methods with job execution logic
|
||||||
|
- Supports both synchronous and asynchronous execution (via `?async=true` query parameter)
|
||||||
|
- Custom timeout configuration per endpoint
|
||||||
|
- Automatic retries with configurable retry count
|
||||||
|
- WebSocket-based progress tracking
|
||||||
|
- Consistent error handling and reporting
|
||||||
|
- Automatic persistence of uploaded files for async processing
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```java
|
||||||
|
@AutoJobPostMapping("/api/v1/security/remove-password")
|
||||||
|
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
String password = request.getPassword();
|
||||||
|
PDDocument document = pdfDocumentFactory.load(fileInput, password);
|
||||||
|
document.setAllSecurityToBeRemoved(true);
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_password_removed.pdf");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
The `AutoJobPostMapping` annotation accepts the following parameters:
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `value` | String[] | `{}` | The path mapping URIs (e.g., "/api/v1/security/add-password") |
|
||||||
|
| `consumes` | String[] | `{"multipart/form-data"}` | Supported media types for requests |
|
||||||
|
| `timeout` | long | `-1` (use system default) | Custom timeout in milliseconds for this job |
|
||||||
|
| `retryCount` | int | `1` (no retries) | Maximum number of retry attempts on failure |
|
||||||
|
| `trackProgress` | boolean | `true` | Enable WebSocket progress tracking for async jobs |
|
||||||
|
| `queueable` | boolean | `false` | Whether this job can be queued when system resources are limited |
|
||||||
|
| `resourceWeight` | int | `50` | Resource weight of this job (1-100), higher values indicate more resource-intensive jobs |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
```java
|
||||||
|
@AutoJobPostMapping("/api/v1/security/remove-password")
|
||||||
|
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Custom Timeout
|
||||||
|
```java
|
||||||
|
// Set a 5-minute timeout for this operation
|
||||||
|
@AutoJobPostMapping(value = "/api/v1/misc/ocr-pdf", timeout = 300000)
|
||||||
|
public ResponseEntity<byte[]> ocrPdf(@ModelAttribute OCRRequest request) {
|
||||||
|
// OCR implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Retries
|
||||||
|
```java
|
||||||
|
// Allow up to 3 retry attempts for external API calls
|
||||||
|
@AutoJobPostMapping(value = "/api/v1/convert/url-to-pdf", retryCount = 3)
|
||||||
|
public ResponseEntity<byte[]> convertUrlToPdf(@ModelAttribute WebsiteToPDFRequest request) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable Progress Tracking
|
||||||
|
```java
|
||||||
|
// Simple, fast operation that doesn't need progress tracking
|
||||||
|
@AutoJobPostMapping(value = "/api/v1/misc/flatten", trackProgress = false)
|
||||||
|
public ResponseEntity<byte[]> flattenPdf(@ModelAttribute FlattenRequest request) {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable Job Queueing for Resource-Intensive Operations
|
||||||
|
```java
|
||||||
|
// Resource-intensive operation that can be queued during high system load
|
||||||
|
@AutoJobPostMapping(
|
||||||
|
value = "/api/v1/misc/ocr-pdf",
|
||||||
|
queueable = true,
|
||||||
|
resourceWeight = 80, // High resource usage
|
||||||
|
timeout = 600000 // 10 minutes
|
||||||
|
)
|
||||||
|
public ResponseEntity<byte[]> ocrPdf(@ModelAttribute OCRRequest request) {
|
||||||
|
// OCR implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lightweight Operation
|
||||||
|
```java
|
||||||
|
// Very lightweight operation with low resource requirements
|
||||||
|
@AutoJobPostMapping(
|
||||||
|
value = "/api/v1/misc/get-page-count",
|
||||||
|
queueable = false,
|
||||||
|
resourceWeight = 10 // Very low resource usage
|
||||||
|
)
|
||||||
|
public ResponseEntity<Integer> getPageCount(@ModelAttribute PDFFile request) {
|
||||||
|
// Simple page count implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client-Side Integration
|
||||||
|
|
||||||
|
For asynchronous jobs, clients can:
|
||||||
|
1. Submit the job with `?async=true` parameter
|
||||||
|
2. Receive a job ID in the response
|
||||||
|
3. Connect to the WebSocket at `/ws/progress/{jobId}` to receive progress updates
|
||||||
|
4. Fetch the completed result from `/api/v1/general/job/{jobId}/result` when done
|
||||||
|
|
||||||
|
Example WebSocket message:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jobId": "b4c9a31d-4b7e-42b2-8ab9-3cbe99d5b94f",
|
||||||
|
"status": "Processing",
|
||||||
|
"progress": 65,
|
||||||
|
"message": "OCR processing page 13/20"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resource-Aware Job Queueing
|
||||||
|
|
||||||
|
The `queueable` parameter enables intelligent resource-aware job queueing for heavy operations. When enabled:
|
||||||
|
|
||||||
|
1. Jobs are automatically queued when system resources (CPU, memory) are constrained
|
||||||
|
2. Queue capacity dynamically adjusts based on available resources
|
||||||
|
3. Queue position and status updates are sent via WebSocket
|
||||||
|
4. Jobs with high `resourceWeight` values have stricter queueing conditions
|
||||||
|
5. Long-running jobs don't block the system from handling other requests
|
||||||
|
|
||||||
|
### Resource Weight Guidelines
|
||||||
|
|
||||||
|
When setting the `resourceWeight` parameter, use these guidelines:
|
||||||
|
|
||||||
|
| Weight Range | Appropriate For |
|
||||||
|
|--------------|----------------|
|
||||||
|
| 1-20 | Lightweight operations: metadata reads, simple transforms, etc. |
|
||||||
|
| 21-50 | Medium operations: basic PDF manipulation, simple image operations |
|
||||||
|
| 51-80 | Heavy operations: PDF merging, image conversions, medium OCR |
|
||||||
|
| 81-100 | Very intensive operations: large OCR jobs, complex transformations |
|
||||||
|
|
||||||
|
### Example Queue Status Messages
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jobId": "b4c9a31d-4b7e-42b2-8ab9-3cbe99d5b94f",
|
||||||
|
"status": "Queued",
|
||||||
|
"progress": 0,
|
||||||
|
"message": "Waiting in queue for resources (position 3)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jobId": "b4c9a31d-4b7e-42b2-8ab9-3cbe99d5b94f",
|
||||||
|
"status": "Starting",
|
||||||
|
"progress": 10,
|
||||||
|
"message": "Resources available, starting job execution"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -23,7 +23,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
"errorOAuth",
|
"errorOAuth",
|
||||||
"file",
|
"file",
|
||||||
"messageType",
|
"messageType",
|
||||||
"infoMessage");
|
"infoMessage",
|
||||||
|
"async");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ public class AnalysisController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/page-count", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/page-count", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get PDF page count",
|
summary = "Get PDF page count",
|
||||||
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
|
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
|
||||||
@@ -39,7 +39,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/basic-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/basic-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get basic PDF information",
|
summary = "Get basic PDF information",
|
||||||
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
|
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
|
||||||
@@ -53,7 +53,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/document-properties", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/document-properties", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get PDF document properties",
|
summary = "Get PDF document properties",
|
||||||
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
|
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
|
||||||
@@ -76,7 +76,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/page-dimensions", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/page-dimensions", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get page dimensions for all pages",
|
summary = "Get page dimensions for all pages",
|
||||||
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
|
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
|
||||||
@@ -96,7 +96,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/form-fields", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/form-fields", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get form field information",
|
summary = "Get form field information",
|
||||||
description =
|
description =
|
||||||
@@ -119,7 +119,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/annotation-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/annotation-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get annotation information",
|
summary = "Get annotation information",
|
||||||
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
|
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
|
||||||
@@ -143,7 +143,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/font-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/font-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get font information",
|
summary = "Get font information",
|
||||||
description =
|
description =
|
||||||
@@ -165,7 +165,7 @@ public class AnalysisController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/security-info", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/security-info", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get security information",
|
summary = "Get security information",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ public class CropController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/crop", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Crops a PDF document",
|
summary = "Crops a PDF document",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.database.DatabaseService;
|
import stirling.software.SPDF.config.security.database.DatabaseService;
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Controller
|
@Controller
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.mail.MailSendException;
|
import org.springframework.mail.MailSendException;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ public class EmailController {
|
|||||||
* attachment.
|
* attachment.
|
||||||
* @return ResponseEntity with success or error message.
|
* @return ResponseEntity with success or error message.
|
||||||
*/
|
*/
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/send-email")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/send-email")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Send an email with an attachment",
|
summary = "Send an email with an attachment",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -110,7 +110,7 @@ public class MergeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Merge multiple PDF files into one",
|
summary = "Merge multiple PDF files into one",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
|||||||
import org.apache.pdfbox.util.Matrix;
|
import org.apache.pdfbox.util.Matrix;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -36,7 +36,7 @@ public class MultiPageLayoutController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Merge multiple pages of a PDF document into a single page",
|
summary = "Merge multiple pages of a PDF document into a single page",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import java.io.IOException;
|
|||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ public class PdfImageRemovalController {
|
|||||||
* content type and filename.
|
* content type and filename.
|
||||||
* @throws IOException If an error occurs while processing the PDF file.
|
* @throws IOException If an error occurs while processing the PDF file.
|
||||||
*/
|
*/
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove images from file to reduce the file size.",
|
summary = "Remove images from file to reduce the file size.",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -39,7 +39,7 @@ public class PdfOverlayController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Overlay PDF files in various modes",
|
summary = "Overlay PDF files in various modes",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -37,7 +37,7 @@ public class RearrangePagesPDFController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-pages")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove pages from a PDF file",
|
summary = "Remove pages from a PDF file",
|
||||||
description =
|
description =
|
||||||
@@ -236,7 +236,7 @@ public class RearrangePagesPDFController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Rearrange pages in a PDF file",
|
summary = "Rearrange pages in a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
|||||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ public class RotationController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Rotate a PDF file",
|
summary = "Rotate a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
|||||||
import org.apache.pdfbox.util.Matrix;
|
import org.apache.pdfbox.util.Matrix;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -37,7 +37,7 @@ public class ScalePagesController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/scale-pages", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Change the size of a PDF page/document",
|
summary = "Change the size of a PDF page/document",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ public class SettingsController {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
@PostMapping("/update-enable-analytics")
|
@AutoJobPostMapping("/update-enable-analytics")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -40,7 +40,7 @@ public class SplitPDFController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split a PDF file into separate documents",
|
summary = "Split a PDF file into separate documents",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -116,7 +116,7 @@ public class SplitPdfByChaptersController {
|
|||||||
return bookmarks;
|
return bookmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split PDFs by Chapters",
|
summary = "Split PDFs by Chapters",
|
||||||
description = "Splits a PDF into chapters and returns a ZIP file.")
|
description = "Splits a PDF into chapters and returns a ZIP file.")
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ import org.apache.pdfbox.util.Matrix;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -43,7 +43,7 @@ public class SplitPdfBySectionsController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split PDF pages into smaller sections",
|
summary = "Split PDF pages into smaller sections",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -38,7 +38,7 @@ public class SplitPdfBySizeController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ public class ToSinglePageController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a multi-page PDF into a single long page PDF",
|
summary = "Convert a multi-page PDF into a single long page PDF",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import stirling.software.SPDF.model.AuthenticationType;
|
|||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ package stirling.software.SPDF.controller.api.converters;
|
|||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -32,7 +32,7 @@ public class ConvertHtmlToPDF {
|
|||||||
|
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+3
-3
@@ -18,7 +18,7 @@ import org.apache.pdfbox.rendering.ImageType;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -49,7 +49,7 @@ public class ConvertImgPDFController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/img")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to image(s)",
|
summary = "Convert PDF to image(s)",
|
||||||
description =
|
description =
|
||||||
@@ -205,7 +205,7 @@ public class ConvertImgPDFController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert images to a PDF file",
|
summary = "Convert images to a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import org.commonmark.renderer.html.AttributeProvider;
|
|||||||
import org.commonmark.renderer.html.HtmlRenderer;
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -41,7 +41,7 @@ public class ConvertMarkdownToPdf {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a Markdown file to PDF",
|
summary = "Convert a Markdown file to PDF",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import org.apache.commons.io.FilenameUtils;
|
|||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -84,7 +84,7 @@ public class ConvertOfficeController {
|
|||||||
return fileExtension.matches(extensionPattern);
|
return fileExtension.matches(extensionPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a file to a PDF using LibreOffice",
|
summary = "Convert a file to a PDF using LibreOffice",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ package stirling.software.SPDF.controller.api.converters;
|
|||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -18,7 +18,7 @@ import stirling.software.common.util.PDFToFile;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertPDFToHtml {
|
public class ConvertPDFToHtml {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to HTML",
|
summary = "Convert PDF to HTML",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+5
-5
@@ -7,7 +7,7 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -34,7 +34,7 @@ public class ConvertPDFToOffice {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Presentation format",
|
summary = "Convert PDF to Presentation format",
|
||||||
description =
|
description =
|
||||||
@@ -49,7 +49,7 @@ public class ConvertPDFToOffice {
|
|||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Text or RTF format",
|
summary = "Convert PDF to Text or RTF format",
|
||||||
description =
|
description =
|
||||||
@@ -77,7 +77,7 @@ public class ConvertPDFToOffice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Word document",
|
summary = "Convert PDF to Word document",
|
||||||
description =
|
description =
|
||||||
@@ -91,7 +91,7 @@ public class ConvertPDFToOffice {
|
|||||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to XML",
|
summary = "Convert PDF to XML",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ import org.apache.commons.io.FileUtils;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -33,7 +33,7 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConvertPDFToPDFA {
|
public class ConvertPDFToPDFA {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a PDF to a PDF/A",
|
summary = "Convert a PDF to a PDF/A",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import java.util.List;
|
|||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ public class ConvertWebsiteToPDF {
|
|||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a URL to a PDF",
|
summary = "Convert a URL to a PDF",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ public class ExtractCSVController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extracts a CSV document from a PDF",
|
summary = "Extracts a CSV document from a PDF",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
|||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -36,7 +36,7 @@ public class FilterController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF contains set text, returns true if does",
|
summary = "Checks if a PDF contains set text, returns true if does",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@@ -54,7 +54,7 @@ public class FilterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF contains an image",
|
summary = "Checks if a PDF contains an image",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@@ -70,7 +70,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@@ -103,7 +103,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is of a certain size",
|
summary = "Checks if a PDF is of a certain size",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@@ -146,7 +146,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is a set file size",
|
summary = "Checks if a PDF is a set file size",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
@@ -179,7 +179,7 @@ public class FilterController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Checks if a PDF is of a certain rotation",
|
summary = "Checks if a PDF is of a certain rotation",
|
||||||
description = "Input:PDF Output:Boolean Type:SISO")
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
|||||||
import org.apache.pdfbox.text.TextPosition;
|
import org.apache.pdfbox.text.TextPosition;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -38,7 +38,7 @@ public class AutoRenameController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract header from PDF file",
|
summary = "Extract header from PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -102,7 +102,7 @@ public class AutoSplitPdfController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Auto split PDF pages into separate documents",
|
summary = "Auto split PDF pages into separate documents",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import org.springframework.http.HttpStatus;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -69,7 +69,7 @@ public class BlankPageController {
|
|||||||
return whitePixelPercentage >= whitePercent;
|
return whitePixelPercentage >= whitePercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove blank pages from a PDF file",
|
summary = "Remove blank pages from a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
|||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -654,7 +654,7 @@ public class CompressController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Optimize PDF file",
|
summary = "Optimize PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -37,7 +37,7 @@ public class DecompressPdfController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/decompress-pdf", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/decompress-pdf", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Decompress PDF streams",
|
summary = "Decompress PDF streams",
|
||||||
description = "Fully decompresses all PDF streams including text content")
|
description = "Fully decompresses all PDF streams including text content")
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -48,7 +48,7 @@ public class ExtractImageScansController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract image scans from an input file",
|
summary = "Extract image scans from an input file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -53,7 +53,7 @@ public class ExtractImagesController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract images from a PDF file",
|
summary = "Extract images from a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import org.apache.pdfbox.rendering.PDFRenderer;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -47,7 +47,7 @@ public class FakeScanController {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private static final Random RANDOM = new Random();
|
private static final Random RANDOM = new Random();
|
||||||
|
|
||||||
@PostMapping(value = "/fake-scan", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/fake-scan", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to look like a scanned document",
|
summary = "Convert PDF to look like a scanned document",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import org.apache.pdfbox.rendering.ImageType;
|
|||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -38,7 +38,7 @@ public class FlattenController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/flatten")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/flatten")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Flatten PDF form fields or full page",
|
summary = "Flatten PDF form fields or full page",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor;
|
import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor;
|
||||||
@@ -51,7 +52,7 @@ public class MetadataController {
|
|||||||
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Update metadata of a PDF file",
|
summary = "Update metadata of a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -60,7 +60,7 @@ public class OCRController {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Process PDF files with OCR using Tesseract",
|
summary = "Process PDF files with OCR using Tesseract",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ import java.io.IOException;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -31,7 +31,7 @@ public class OverlayImageController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-image")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-image")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Overlay image onto a PDF file",
|
summary = "Overlay image onto a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -37,7 +37,7 @@ public class PageNumbersController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add page numbers to a PDF document",
|
summary = "Add page numbers to a PDF document",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
|||||||
public class PrintFileController {
|
public class PrintFileController {
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// @PostMapping(value = "/print-file", consumes = "multipart/form-data")
|
// @AutoJobPostMapping(value = "/print-file", consumes = "multipart/form-data")
|
||||||
// @Operation(
|
// @Operation(
|
||||||
// summary = "Prints PDF/Image file to a set printer",
|
// summary = "Prints PDF/Image file to a set printer",
|
||||||
// description =
|
// description =
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -33,7 +33,7 @@ public class RepairController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/repair")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Repair a PDF file",
|
summary = "Repair a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ public class ReplaceAndInvertColorController {
|
|||||||
|
|
||||||
private final ReplaceAndInvertColorService replaceAndInvertColorService;
|
private final ReplaceAndInvertColorService replaceAndInvertColorService;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Replace-Invert Color PDF",
|
summary = "Replace-Invert Color PDF",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -32,7 +32,7 @@ public class ShowJavascript {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
||||||
description = "desc. Input:PDF Output:JS Type:SISO")
|
description = "desc. Input:PDF Output:JS Type:SISO")
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import org.apache.pdfbox.util.Matrix;
|
|||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -50,7 +50,7 @@ public class StampController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add stamp to a PDF file",
|
summary = "Add stamp to a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ public class UnlockPDFFormsController {
|
|||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/unlock-pdf-forms")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove read-only property from form fields",
|
summary = "Remove read-only property from form fields",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import org.springframework.core.io.Resource;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -46,7 +46,7 @@ public class PipelineController {
|
|||||||
|
|
||||||
private final PostHogService postHogService;
|
private final PostHogService postHogService;
|
||||||
|
|
||||||
@PostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@AutoJobPostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||||
throws JsonMappingException, JsonProcessingException {
|
throws JsonMappingException, JsonProcessingException {
|
||||||
MultipartFile[] files = request.getFileInput();
|
MultipartFile[] files = request.getFileInput();
|
||||||
|
|||||||
+2
-2
@@ -59,7 +59,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -136,7 +136,7 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(
|
@AutoJobPostMapping(
|
||||||
consumes = {
|
consumes = {
|
||||||
MediaType.MULTIPART_FORM_DATA_VALUE,
|
MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE
|
MediaType.APPLICATION_FORM_URLENCODED_VALUE
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import org.apache.xmpbox.xml.XmpSerializer;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -118,7 +118,7 @@ public class GetInfoOnPDF {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||||
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
||||||
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
|||||||
+3
-3
@@ -7,7 +7,7 @@ import org.apache.pdfbox.pdmodel.encryption.AccessPermission;
|
|||||||
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
|
import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -31,7 +31,7 @@ public class PasswordController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove password from a PDF file",
|
summary = "Remove password from a PDF file",
|
||||||
description =
|
description =
|
||||||
@@ -50,7 +50,7 @@ public class PasswordController {
|
|||||||
+ "_password_removed.pdf");
|
+ "_password_removed.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add password to a PDF file",
|
summary = "Add password to a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -56,7 +56,7 @@ public class RedactController {
|
|||||||
List.class, "redactions", new StringToArrayListPropertyEditor());
|
List.class, "redactions", new StringToArrayListPropertyEditor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/redact", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/redact", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redacts areas and pages in a PDF document",
|
summary = "Redacts areas and pages in a PDF document",
|
||||||
description =
|
description =
|
||||||
@@ -190,7 +190,7 @@ public class RedactController {
|
|||||||
return pageNumbers;
|
return pageNumbers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
@AutoJobPostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redacts listOfText in a PDF document",
|
summary = "Redacts listOfText in a PDF document",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -32,7 +32,7 @@ public class RemoveCertSignController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove digital signature from PDF",
|
summary = "Remove digital signature from PDF",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -37,7 +37,7 @@ public class SanitizeController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sanitize a PDF file",
|
summary = "Sanitize a PDF file",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -68,7 +68,7 @@ public class ValidateSignatureController {
|
|||||||
description =
|
description =
|
||||||
"Validates the digital signatures in a PDF file against default or custom"
|
"Validates the digital signatures in a PDF file against default or custom"
|
||||||
+ " certificates. Input:PDF Output:JSON Type:SISO")
|
+ " certificates. Input:PDF Output:JSON Type:SISO")
|
||||||
@PostMapping(value = "/validate-signature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@AutoJobPostMapping(value = "/validate-signature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
||||||
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
||||||
List<SignatureValidationResult> results = new ArrayList<>();
|
List<SignatureValidationResult> results = new ArrayList<>();
|
||||||
|
|||||||
+4
-3
@@ -28,7 +28,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -64,7 +64,7 @@ public class WatermarkController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add watermark to a PDF file",
|
summary = "Add watermark to a PDF file",
|
||||||
description =
|
description =
|
||||||
@@ -177,7 +177,8 @@ public class WatermarkController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!"".equals(resourceDir)) {
|
if (!"".equals(resourceDir)) {
|
||||||
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
ClassPathResource classPathResource = new ClassPathResource(resourceDir, getClass().getClassLoader());
|
||||||
|
|
||||||
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
||||||
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
|
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
|
||||||
try (InputStream is = classPathResource.getInputStream();
|
try (InputStream is = classPathResource.getInputStream();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package stirling.software.SPDF.model.api.converters;
|
|||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -18,7 +18,7 @@ import stirling.software.common.util.PDFToFile;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertPDFToMarkdown {
|
public class ConvertPDFToMarkdown {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/markdown")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/markdown")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to Markdown",
|
summary = "Convert PDF to Markdown",
|
||||||
description =
|
description =
|
||||||
|
|||||||
+577
@@ -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"
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
# ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| #
|
# ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| #
|
||||||
# |____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| #
|
# |____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| #
|
||||||
# #
|
# #
|
||||||
# Custom setting.yml file with all endpoints disabled to only be used for testing purposes #
|
|
||||||
# Do not comment out any entry, it will be removed on next startup #
|
# Do not comment out any entry, it will be removed on next startup #
|
||||||
# If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME #
|
# If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME #
|
||||||
#############################################################################################################
|
#############################################################################################################
|
||||||
@@ -65,12 +64,13 @@ premium:
|
|||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
enabled: false # Enable license key checks for pro/enterprise features
|
enabled: false # Enable license key checks for pro/enterprise features
|
||||||
proFeatures:
|
proFeatures:
|
||||||
|
database: true # Enable database features
|
||||||
SSOAutoLogin: false
|
SSOAutoLogin: false
|
||||||
CustomMetadata:
|
CustomMetadata:
|
||||||
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
|
autoUpdateMetadata: false
|
||||||
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
|
author: username
|
||||||
creator: Stirling-PDF # supports text such as 'Company-PDF'
|
creator: Stirling-PDF
|
||||||
producer: Stirling-PDF # supports text such as 'Company-PDF'
|
producer: Stirling-PDF
|
||||||
googleDrive:
|
googleDrive:
|
||||||
enabled: false
|
enabled: false
|
||||||
clientId: ''
|
clientId: ''
|
||||||
@@ -127,7 +127,7 @@ ui:
|
|||||||
appNameNavbar: '' # name displayed on the navigation bar
|
appNameNavbar: '' # name displayed on the navigation bar
|
||||||
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||||
|
|
||||||
endpoints: # All the possible endpoints are disabled
|
endpoints:
|
||||||
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
||||||
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ metrics:
|
|||||||
AutomaticallyGenerated:
|
AutomaticallyGenerated:
|
||||||
key: cbb81c0f-50b1-450c-a2b5-89ae527776eb
|
key: cbb81c0f-50b1-450c-a2b5-89ae527776eb
|
||||||
UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a
|
UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a
|
||||||
appVersion: 0.44.3
|
appVersion: 0.46.2
|
||||||
|
|
||||||
processExecutor:
|
processExecutor:
|
||||||
sessionLimit: # Process executor instances limits
|
sessionLimit: # Process executor instances limits
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Feature: API Validation
|
|||||||
| password | wrongPassword |
|
| password | wrongPassword |
|
||||||
When I send the API request to the endpoint "/api/v1/security/remove-password"
|
When I send the API request to the endpoint "/api/v1/security/remove-password"
|
||||||
Then the response status code should be 500
|
Then the response status code should be 500
|
||||||
And the response should contain error message "Internal Server Error"
|
And the response should contain error message "Job failed: org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException: Cannot decrypt PDF, the password is incorrect"
|
||||||
|
|
||||||
@positive @info
|
@positive @info
|
||||||
Scenario: Get info
|
Scenario: Get info
|
||||||
|
|||||||
Reference in New Issue
Block a user