feat(docker): update base images to Java 25, Spring 4, Jackson 3, Gradle 9 and optimize JVM options (Project Lilliput) (#5725)

Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
Balázs Szücs
2026-02-24 20:06:32 +00:00
committed by GitHub
co-authored by Anthony Stirling
parent 24128dd318
commit 1f9b90ad57
112 changed files with 3181 additions and 2102 deletions
+3 -3
View File
@@ -27,8 +27,8 @@ spotless {
}
}
dependencies {
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-aop'
api 'org.springframework.boot:spring-boot-starter-webmvc'
api 'org.springframework.boot:spring-boot-starter-aspectj'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
@@ -42,7 +42,7 @@ dependencies {
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15"
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1"
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
api 'org.simplejavamail:simple-java-mail:8.12.6'
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
@@ -91,9 +91,9 @@ public class RuntimePathConfig {
// Initialize Operation paths
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
String defaultUnoConvertPath = isDocker ? "/usr/local/bin/unoconvert" : "unoconvert";
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
String defaultOcrMyPdfPath = isDocker ? "/opt/venv/bin/ocrmypdf" : "ocrmypdf";
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
Operations operations = customPaths.getOperations();
@@ -0,0 +1,23 @@
package stirling.software.common.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
/**
* Configures the scheduler used by all {@code @Scheduled} methods. Uses virtual threads so that
* long-running scheduled tasks (e.g. cleanup, license checks, file monitoring) never block each
* other — each runs on its own lightweight virtual thread.
*/
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
scheduler.setVirtualThreads(true);
scheduler.setThreadNamePrefix("scheduled-vt-");
return scheduler;
}
}
@@ -35,7 +35,7 @@ public class JobExecutorService {
private final HttpServletRequest request;
private final ResourceMonitor resourceMonitor;
private final JobQueue jobQueue;
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
private final ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
private final long effectiveTimeoutMs;
@Autowired(required = false)
@@ -44,8 +44,10 @@ public class JobQueue implements SmartLifecycle {
private volatile BlockingQueue<QueuedJob> jobQueue;
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("job-queue-scheduler-", 0).factory());
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualThreadExecutor();
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
private boolean shuttingDown = false;
@@ -43,7 +43,9 @@ public class ResourceMonitor {
@Value("${stirling.resource.monitor.interval-ms:60000}")
private long monitorIntervalMs = 60000; // 60 seconds
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("resource-monitor-", 0).factory());
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
@@ -42,7 +42,8 @@ public class TaskManager {
private final FileStorage fileStorage;
private final ScheduledExecutorService cleanupExecutor =
Executors.newSingleThreadScheduledExecutor();
Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("task-cleanup-", 0).factory());
/** Initialize the task manager and start the cleanup scheduler */
public TaskManager(FileStorage fileStorage) {
@@ -2,30 +2,28 @@ package stirling.software.common.util;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import lombok.extern.slf4j.Slf4j;
/**
* Factory for creating executors backed by virtual threads (Java 21+). Virtual threads are
* lightweight, managed by the JVM, and ideal for I/O-bound tasks. They eliminate the need for
* thread pool sizing since thousands can run concurrently with minimal overhead.
*/
public final class ExecutorFactory {
@Slf4j
public class ExecutorFactory {
private ExecutorFactory() {}
/** Creates an {@link ExecutorService} that starts a new virtual thread for each task. */
public static ExecutorService newVirtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
/**
* Creates an ExecutorService using virtual threads if available (Java 21+), or falls back to a
* cached thread pool on older Java versions.
* Creates a {@link ScheduledExecutorService} backed by a single virtual thread. Useful for
* periodic/delayed tasks that should not pin a platform thread.
*/
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();
public static ScheduledExecutorService newSingleVirtualThreadScheduledExecutor() {
return Executors.newSingleThreadScheduledExecutor(
Thread.ofVirtual().name("scheduled-vt-", 0).factory());
}
}
@@ -68,6 +68,17 @@ public class FileMonitor {
if (this.rootDirs.isEmpty()) {
log.error("No valid directories to monitor - FileMonitor will not function");
} else {
// Register directories eagerly so the first @Scheduled tick does not warn.
for (Path rootDir : this.rootDirs) {
if (Files.exists(rootDir)) {
try {
recursivelyRegisterEntry(rootDir);
} catch (IOException e) {
log.error("Failed to register monitoring for {}", rootDir, e);
}
}
}
}
}
@@ -649,7 +649,8 @@ public class GeneralUtils {
}
public List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
List<Integer> result = new ArrayList<>();
// Use LinkedHashSet to prevent duplicates from inflating size and triggering maxSize guard
Set<Integer> result = new LinkedHashSet<>();
int offset = oneBased ? 1 : 0;
int maxSize = Math.max(1000, totalPages * 3);
for (String page : pages) {
@@ -672,7 +673,7 @@ public class GeneralUtils {
"Page list exceeds maximum allowed size of " + maxSize);
}
}
return result;
return new ArrayList<>(result);
}
/*
@@ -226,50 +226,54 @@ public class ProcessExecutor {
List<String> outputLines = new ArrayList<>();
Thread errorReaderThread =
new Thread(
() -> {
try (BufferedReader errorReader =
new BufferedReader(
new InputStreamReader(
process.getErrorStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
errorReader, 5_000_000))
!= null) {
errorLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn("Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
Thread.ofVirtual()
.unstarted(
() -> {
try (BufferedReader errorReader =
new BufferedReader(
new InputStreamReader(
process.getErrorStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
errorReader, 5_000_000))
!= null) {
errorLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn(
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
Thread outputReaderThread =
new Thread(
() -> {
try (BufferedReader outputReader =
new BufferedReader(
new InputStreamReader(
process.getInputStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
outputReader, 5_000_000))
!= null) {
outputLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn("Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
Thread.ofVirtual()
.unstarted(
() -> {
try (BufferedReader outputReader =
new BufferedReader(
new InputStreamReader(
process.getInputStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line =
BoundedLineReader.readLine(
outputReader, 5_000_000))
!= null) {
outputLines.add(line);
if (liveUpdates) log.info(line);
}
} catch (InterruptedIOException e) {
log.warn(
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
log.error("exception", e);
}
});
errorReaderThread.start();
outputReaderThread.start();
@@ -370,7 +374,7 @@ public class ProcessExecutor {
}
// Match common unoconvert variants (but NOT soffice)
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
if (lowerBasename.contains("unoconvert") || "unoconv".equals(lowerBasename)) {
return true;
}
}
@@ -4,18 +4,23 @@ import java.beans.PropertyEditorSupport;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.api.security.RedactionArea;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@Slf4j
public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper =
JsonMapper.builder()
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
@Override
public void setAsText(String text) throws IllegalArgumentException {
@@ -24,7 +29,6 @@ public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
return;
}
try {
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
TypeReference<ArrayList<RedactionArea>> typeRef = new TypeReference<>() {};
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
setValue(list);
@@ -4,12 +4,13 @@ import java.beans.PropertyEditorSupport;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
public class StringToMapPropertyEditor extends PropertyEditorSupport {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
@Override
public void setAsText(String text) throws IllegalArgumentException {
@@ -12,11 +12,12 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
public StrategyType lastStrategyUsed;
public SpyPDFDocumentFactory(PdfMetadataService service) {
super(service);
super(service); // TempFileManager falls back to Files.createTempFile in tests
}
@Override
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
protected StreamCacheCreateFunction getStreamCacheFunction(
long contentSize, MemorySnapshot mem) {
StrategyType type;
if (contentSize < 10 * 1024 * 1024) {
type = StrategyType.MEMORY_ONLY;
@@ -26,6 +27,6 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
type = StrategyType.TEMP_FILE;
}
this.lastStrategyUsed = type;
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
return super.getStreamCacheFunction(contentSize, mem);
}
}
@@ -121,17 +121,19 @@ public class UnoServerPoolTest {
// Try to acquire a third endpoint in separate thread (should block)
Thread blockingThread =
new Thread(
() -> {
try {
acquireLatch.countDown(); // Signal we're about to block
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint();
acquired.incrementAndGet();
lease3.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread.ofVirtual()
.unstarted(
() -> {
try {
acquireLatch.countDown(); // Signal we're about to block
UnoServerPool.UnoServerLease lease3 =
pool.acquireEndpoint();
acquired.incrementAndGet();
lease3.close();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
blockingThread.start();
acquireLatch.await(); // Wait for thread to start
@@ -143,11 +143,21 @@ class StringToArrayListPropertyEditorTest {
}
@Test
void testSetAsText_InvalidStructure() {
// Arrange - this JSON doesn't match RedactionArea structure
void testSetAsText_UnknownProperties() {
// Arrange - this JSON contains properties not in RedactionArea
// With FAIL_ON_UNKNOWN_PROPERTIES disabled, this should ignore the unknown properties
String json = "[{\"invalid\":\"structure\"}]";
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
// Act
editor.setAsText(json);
Object value = editor.getValue();
// Assert
assertNotNull(value, "Value should not be null");
assertInstanceOf(List.class, value, "Value should be a List");
@SuppressWarnings("unchecked")
List<RedactionArea> list = (List<RedactionArea>) value;
assertEquals(1, list.size(), "List should have 1 entry (empty object)");
}
}
+20 -4
View File
@@ -46,8 +46,23 @@ dependencies {
implementation project(':common')
implementation 'org.springframework.boot:spring-boot-starter-jetty'
implementation 'com.posthog.java:posthog:1.2.0'
implementation 'org.telegram:telegrambots:6.9.7.1'
implementation 'org.eclipse.jetty.http2:jetty-http2-server'
implementation ('org.telegram:telegrambots:6.9.7.1') {
// Grizzly server + Jersey JAX-RS stack: only used for webhook mode;
// Stirling-PDF uses long-polling mode so these are dead weight (~3 MB)
exclude group: 'org.glassfish.jersey.inject'
exclude group: 'org.glassfish.jersey.media'
exclude group: 'org.glassfish.jersey.containers'
exclude group: 'org.glassfish.jersey.core'
exclude group: 'org.glassfish.jersey.ext'
exclude group: 'org.glassfish.grizzly'
exclude group: 'org.glassfish.hk2'
exclude group: 'org.glassfish.hk2.external'
exclude group: 'org.javassist', module: 'javassist'
// Old javax JAX-RS Jackson bindings (not needed, project uses jakarta)
exclude group: 'com.fasterxml.jackson.jaxrs'
exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations'
}
implementation 'commons-io:commons-io:2.21.0'
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
@@ -78,8 +93,9 @@ dependencies {
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation 'org.apache.poi:poi-ooxml:5.5.1'
// Batik
implementation 'org.apache.xmlgraphics:batik-all:1.19'
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
// Replaces batik-all which included unused codec, svggen, transcoder, script modules
implementation 'org.apache.xmlgraphics:batik-bridge:1.19'
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
@@ -49,8 +49,8 @@ public class LibreOfficeListener {
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener");
lastActivityTime = System.currentTimeMillis();
// Start a background thread to monitor the activity timeout
executorService = Executors.newSingleThreadExecutor();
// Start a virtual thread to monitor the activity timeout
executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.submit(
() -> {
while (true) {
@@ -13,7 +13,7 @@ import java.util.regex.Pattern;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
@@ -176,11 +176,14 @@ public class SPDFApplication {
}
@EventListener
public void onWebServerInitialized(WebServerInitializedEvent event) {
int actualPort = event.getWebServer().getPort();
serverPortStatic = String.valueOf(actualPort);
public void onApplicationReady(ApplicationReadyEvent event) {
String port =
event.getApplicationContext().getEnvironment().getProperty("local.server.port");
if (port != null) {
serverPortStatic = port;
}
// Log the actual runtime port for Tauri to parse
log.info("Stirling-PDF running on port: {}", actualPort);
log.info("Stirling-PDF running on port: {}", serverPortStatic);
}
private static void printStartupLogs() {
@@ -51,9 +51,7 @@ public class ExternalAppDepConfig {
*/
private final Map<String, List<String>> commandToGroupMapping;
private final ExecutorService pool =
Executors.newFixedThreadPool(
Math.max(2, Runtime.getRuntime().availableProcessors() / 2));
private final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
public ExternalAppDepConfig(
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
@@ -1,7 +1,7 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.boot.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
@@ -54,8 +54,8 @@ public class TauriProcessMonitor {
scheduler =
Executors.newSingleThreadScheduledExecutor(
r -> {
Thread t = new Thread(r, "tauri-process-monitor");
t.setDaemon(true);
Thread t =
Thread.ofVirtual().name("tauri-process-monitor").unstarted(r);
return t;
});
@@ -154,8 +154,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
.maxAge(3600);
} else {
// Default to allowing all origins when nothing is configured
logger.info(
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
logger.debug(
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); WebMvcConfig allowing all origins.");
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
@@ -15,9 +15,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Getter;
@@ -32,6 +29,9 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@GeneralApi
@Slf4j
@RequiredArgsConstructor
@@ -16,9 +16,6 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Data;
@@ -35,6 +32,9 @@ import stirling.software.common.service.UserServiceInterface;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@UiDataApi
public class UIDataController {
@@ -44,18 +44,21 @@ public class UIDataController {
private final UserServiceInterface userService;
private final ResourceLoader resourceLoader;
private final RuntimePathConfig runtimePathConfig;
private final ObjectMapper objectMapper;
public UIDataController(
ApplicationProperties applicationProperties,
SharedSignatureService signatureService,
@Autowired(required = false) UserServiceInterface userService,
ResourceLoader resourceLoader,
RuntimePathConfig runtimePathConfig) {
RuntimePathConfig runtimePathConfig,
ObjectMapper objectMapper) {
this.applicationProperties = applicationProperties;
this.signatureService = signatureService;
this.userService = userService;
this.resourceLoader = resourceLoader;
this.runtimePathConfig = runtimePathConfig;
this.objectMapper = objectMapper;
}
@GetMapping("/footer-info")
@@ -93,9 +96,8 @@ public class UIDataController {
try (InputStream is = resource.getInputStream()) {
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
Map<String, List<Dependency>> licenseData =
mapper.readValue(json, new TypeReference<>() {});
objectMapper.readValue(json, new TypeReference<>() {});
data.setDependencies(licenseData.get("dependencies"));
} catch (IOException e) {
log.error("Failed to load licenses data", e);
@@ -127,8 +129,8 @@ public class UIDataController {
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent =
new ObjectMapper()
.readValue(config, new TypeReference<Map<String, Object>>() {});
objectMapper.readValue(
config, new TypeReference<Map<String, Object>>() {});
String name = (String) jsonContent.get("name");
if (name == null || name.length() < 1) {
String filename =
@@ -91,22 +91,32 @@ public class ConvertOfficeController {
Path libreOfficeProfile = null;
try {
ProcessExecutorResult result;
// Run Unoconvert command
if (isUnoconvertAvailable()) {
// Unoconvert: schreibe direkt in outputPath innerhalb des workDir
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getUnoConvertPath());
command.add("--convert-to");
command.add("pdf");
command.add(inputPath.toString());
command.add(outputPath.toString());
ProcessExecutorResult result = null;
IOException unoconvertException = null;
result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
} // Run soffice command
else {
// Try unoconvert first if available
if (isUnoconvertAvailable()) {
try {
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getUnoConvertPath());
command.add("--convert-to");
command.add("pdf");
command.add(inputPath.toString());
command.add(outputPath.toString());
result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
} catch (IOException e) {
unoconvertException = e;
log.warn(
"Unoconvert command failed ({}). Falling back to soffice command.",
e.getMessage());
}
}
// Fallback to soffice if unoconvert was unavailable or failed
if (result == null) {
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getSOfficePath());
@@ -114,14 +124,21 @@ public class ConvertOfficeController {
command.add("--headless");
command.add("--nologo");
command.add("--convert-to");
command.add("pdf:writer_pdf_Export");
command.add("pdf");
command.add("--outdir");
command.add(workDir.toString());
command.add(inputPath.toString());
result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
try {
result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
} catch (IOException e) {
if (unoconvertException != null) {
e.addSuppressed(unoconvertException);
}
throw e;
}
}
// Check the result
@@ -70,10 +70,7 @@ public class ConvertPDFToExcelController {
tables.size() == 1
? String.format(Locale.ROOT, "Page %d", pageNum)
: String.format(
Locale.ROOT,
"Page %d Table %d",
pageNum,
tableIdx + 1);
Locale.ROOT, "Page %d Table %d", pageNum, tableIdx + 1);
sheetName = getUniqueSheetName(workbook, sheetName);
Sheet sheet = workbook.createSheet(sheetName);
@@ -9,13 +9,13 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.exception.CacheUnavailableException;
import tools.jackson.databind.ObjectMapper;
@ControllerAdvice(assignableTypes = ConvertPdfJsonController.class)
@Slf4j
@RequiredArgsConstructor
@@ -19,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.opencsv.CSVWriter;
import io.github.pixee.security.Filenames;
@@ -38,6 +36,9 @@ import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.FormUtils;
import stirling.software.common.util.WebResponseUtils;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("/api/v1/form")
@Tag(
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.form;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -8,13 +7,14 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.FormUtils;
import tools.jackson.core.JacksonException;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
final class FormPayloadParser {
private static final String KEY_FIELDS = "fields";
@@ -32,8 +32,7 @@ final class FormPayloadParser {
private FormPayloadParser() {}
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json)
throws IOException {
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json) {
if (json == null || json.isBlank()) {
return Map.of();
}
@@ -41,7 +40,7 @@ final class FormPayloadParser {
JsonNode root;
try {
root = objectMapper.readTree(json);
} catch (IOException e) {
} catch (JacksonException e) {
// Fallback to legacy direct map parse (will throw again if invalid)
return objectMapper.readValue(json, MAP_TYPE);
}
@@ -88,14 +87,14 @@ final class FormPayloadParser {
}
static List<FormUtils.ModifyFormFieldDefinition> parseModificationDefinitions(
ObjectMapper objectMapper, String json) throws IOException {
ObjectMapper objectMapper, String json) {
if (json == null || json.isBlank()) {
return List.of();
}
return objectMapper.readValue(json, MODIFY_FIELD_LIST_TYPE);
}
static List<String> parseNameList(ObjectMapper objectMapper, String json) throws IOException {
static List<String> parseNameList(ObjectMapper objectMapper, String json) {
if (json == null || json.isBlank()) {
return List.of();
}
@@ -119,7 +118,7 @@ final class FormPayloadParser {
}
}
} else if (root.isTextual()) {
final String single = trimToNull(root.asText());
final String single = trimToNull(root.asText(""));
if (single != null) {
names.add(single);
}
@@ -131,7 +130,7 @@ final class FormPayloadParser {
try {
return objectMapper.readValue(json, STRING_LIST_TYPE);
} catch (IOException e) {
} catch (JacksonException e) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidFormat",
"Invalid {0} format: {1}",
@@ -199,7 +198,7 @@ final class FormPayloadParser {
return null;
}
if (node.isTextual()) {
return trimToEmpty(node.asText());
return trimToEmpty(node.asText(""));
}
if (node.isNumber()) {
return node.numberValue().toString();
@@ -208,7 +207,7 @@ final class FormPayloadParser {
return Boolean.toString(node.booleanValue());
}
// Fallback for other scalar-like nodes
return trimToEmpty(node.asText());
return trimToEmpty(node.asText(""));
}
private static void collectNames(JsonNode arrayNode, Set<String> sink) {
@@ -229,7 +228,7 @@ final class FormPayloadParser {
}
if (node.isTextual()) {
return trimToNull(node.asText());
return trimToNull(node.asText(""));
}
if (node.isObject()) {
@@ -264,8 +263,8 @@ final class FormPayloadParser {
private static Map<String, Object> objectToLinkedMap(JsonNode objectNode) {
final Map<String, Object> result = new LinkedHashMap<>();
objectNode
.fieldNames()
.forEachRemaining(
.propertyNames()
.forEach(
key -> {
final JsonNode v = objectNode.get(key);
if (v == null || v.isNull()) {
@@ -82,9 +82,8 @@ public class ExtractImagesController {
Set<byte[]> processedImages = new HashSet<>();
if (useMultithreading) {
// Executor service to handle multithreading
ExecutorService executor =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// Virtual thread executor lightweight threads ideal for I/O-bound image extraction
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Set<Future<Void>> futures = new HashSet<>();
// Safely iterate over each page, handling corrupt PDFs where page count might be wrong
@@ -246,10 +246,12 @@ public class OCRController {
command.add("--clean-final");
}
if (ocrType != null && !ocrType.isEmpty()) {
if ("skip-text".equals(ocrType)) {
command.add("--skip-text");
} else if ("force-ocr".equals(ocrType)) {
if ("force-ocr".equals(ocrType)) {
command.add("--force-ocr");
} else {
// Default for 'Normal' and 'skip-text': use --skip-text
// ocrmypdf 17+ requires explicit flag when PDF already contains text
command.add("--skip-text");
}
}
command.add("--invalidate-digital-signatures");
@@ -378,11 +380,12 @@ public class OCRController {
List<String> command = new ArrayList<>();
command.add("tesseract");
command.add(imagePath.toString());
command.add(
String outputBase =
new File(
tempOutputDir,
String.format(Locale.ROOT, "page_%d", pageNum))
.toString());
.toString();
command.add(outputBase);
command.add("-l");
command.add(String.join("+", selectedLanguages));
command.add("pdf"); // Always output PDF
@@ -400,6 +403,18 @@ public class OCRController {
result.getRc());
}
// Verify the OCR'd PDF was created
if (!pageOutputPath.exists()) {
log.warn(
"Tesseract did not create expected output file: {}. Page may be blank or unreadable.",
pageOutputPath.getAbsolutePath());
// Save original page without OCR as fallback
try (PDDocument pageDoc = new PDDocument()) {
pageDoc.addPage(page);
pageDoc.save(pageOutputPath);
}
}
// Add OCR'd PDF to merger
merger.addSource(pageOutputPath);
} else {
@@ -14,10 +14,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
@@ -34,6 +30,10 @@ import stirling.software.common.service.PostHogService;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.DatabindException;
import tools.jackson.databind.ObjectMapper;
@PipelineApi
@Slf4j
@RequiredArgsConstructor
@@ -54,7 +54,7 @@ public class PipelineController {
+ "Users provide files and a JSON configuration defining the sequence of operations to perform. "
+ "Input:PDF Output:PDF/ZIP Type:MIMO")
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
throws JsonMappingException, JsonProcessingException {
throws DatabindException, JacksonException {
MultipartFile[] files = request.getFileInput();
String jsonString = request.getJson();
if (files == null) {
@@ -28,8 +28,6 @@ import org.springframework.core.io.Resource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PipelineConfig;
@@ -40,6 +38,8 @@ import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.service.PostHogService;
import stirling.software.common.util.FileMonitor;
import tools.jackson.databind.ObjectMapper;
@Service
@Slf4j
public class PipelineDirectoryProcessor {
@@ -47,10 +47,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
@@ -66,6 +62,11 @@ import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.WebResponseUtils;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
@SecurityApi
@Slf4j
@RequiredArgsConstructor
@@ -77,7 +78,7 @@ public class GetInfoOnPDF {
private static final String PAGE_PREFIX = "Page ";
private static final long MAX_FILE_SIZE = 100L * 1024 * 1024;
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectMapper objectMapper = JsonMapper.builder().build();
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final VeraPDFService veraPDFService;
@@ -1,5 +1,7 @@
package stirling.software.SPDF.controller.web;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
@@ -54,10 +56,27 @@ public class MetricsController {
}
Map<String, String> status = new HashMap<>();
status.put("status", "UP");
status.put("version", getClass().getPackage().getImplementationVersion());
String version = getClass().getPackage().getImplementationVersion();
if (version == null) {
version = getVersionFromProperties();
}
status.put("version", version);
return ResponseEntity.ok(status);
}
private String getVersionFromProperties() {
try (InputStream is = getClass().getResourceAsStream("/version.properties")) {
if (is != null) {
Properties props = new Properties();
props.load(is);
return props.getProperty("version");
}
} catch (IOException e) {
log.error("Failed to load version.properties", e);
}
return null;
}
@GetMapping("/load")
@Operation(
summary = "GET request count",
@@ -190,6 +190,31 @@ public class GlobalExceptionHandler {
return problemDetail;
}
/**
* Checks whether the given IOException indicates that the client disconnected before the
* response could be written (broken pipe, connection reset, etc.). When this happens there is
* no point in serialising a {@link ProblemDetail} body because the socket is already closed
* and attempting to do so may trigger a secondary {@code HttpMessageNotWritableException} if
* the response Content-Type was already committed as a non-JSON type (e.g. image/png).
*/
private static boolean isClientDisconnectException(IOException ex) {
// Walk the causal chain Jetty/Tomcat may wrap the low-level SocketException
Throwable current = ex;
while (current != null) {
String msg = current.getMessage();
if (msg != null) {
String lower = msg.toLowerCase(java.util.Locale.ROOT);
if (lower.contains("broken pipe")
|| lower.contains("connection reset")
|| lower.contains("an established connection was aborted")) {
return true;
}
}
current = current.getCause();
}
return false;
}
/**
* Helper method to create a standardized ProblemDetail response for exceptions with error
* codes.
@@ -1107,6 +1132,15 @@ public class GlobalExceptionHandler {
public ResponseEntity<ProblemDetail> handleIOException(
IOException ex, HttpServletRequest request) {
// Broken pipe / connection reset means the client disconnected.
// Attempting to write a ProblemDetail response will fail because the
// response Content-Type may already be committed (e.g. image/png) and
// the client is gone anyway. Log at WARN and return an empty body.
if (isClientDisconnectException(ex)) {
log.warn("Client disconnected at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
// Check if this is a PDF-specific error and wrap it appropriately
IOException processedException =
ExceptionUtils.handlePdfException(ex, request.getRequestURI());
@@ -3,10 +3,10 @@ package stirling.software.SPDF.model;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import tools.jackson.databind.JsonNode;
public class ApiEndpoint {
private final String name;
private Map<String, JsonNode> parameters;
@@ -18,10 +18,10 @@ public class ApiEndpoint {
postNode.path("parameters")
.forEach(
paramNode -> {
String paramName = paramNode.path("name").asText();
String paramName = paramNode.path("name").asText("");
parameters.put(paramName, paramNode);
});
this.description = postNode.path("description").asText();
this.description = postNode.path("description").asText("");
}
public boolean areParametersValid(Map<String, Object> providedParams) {
@@ -15,9 +15,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j;
@@ -28,6 +25,9 @@ import stirling.software.common.model.enumeration.Role;
import stirling.software.common.service.UserServiceInterface;
import stirling.software.common.util.RegexPatternUtils;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
@Service
@Slf4j
public class ApiDocService {
@@ -36,12 +36,15 @@ public class ApiDocService {
private final ServletContext servletContext;
private final UserServiceInterface userService;
private final ObjectMapper objectMapper;
Map<String, List<String>> outputToFileTypes = new HashMap<>();
JsonNode apiDocsJsonRootNode;
public ApiDocService(
ObjectMapper objectMapper,
ServletContext servletContext,
@Autowired(required = false) UserServiceInterface userService) {
this.objectMapper = objectMapper;
this.servletContext = servletContext;
this.userService = userService;
}
@@ -116,8 +119,7 @@ public class ApiDocService {
ResponseEntity<String> response =
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
apiDocsJson = response.getBody();
ObjectMapper mapper = new ObjectMapper();
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
apiDocsJsonRootNode = objectMapper.readTree(apiDocsJson);
JsonNode paths = apiDocsJsonRootNode.path("paths");
paths.propertyStream()
.forEach(
@@ -427,7 +427,8 @@ public class CertificateValidationService {
try (InputStream certStream =
getClass().getClassLoader().getResourceAsStream("certs/cacert.pem")) {
if (certStream == null) {
log.warn("Bundled Mozilla CA certificate file not found in resources");
log.debug(
"Bundled Mozilla CA certificate file not found in resources — using Java system trust store only");
return;
}
@@ -90,8 +90,6 @@ import org.apache.pdfbox.util.Matrix;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@@ -129,6 +127,8 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@Service
@RequiredArgsConstructor
@@ -6834,7 +6834,8 @@ public class PdfJsonConversionService {
/** Schedules automatic cleanup of cached documents after 30 minutes. */
private void scheduleDocumentCleanup(String jobId) {
new Thread(
Thread.ofVirtual()
.start(
() -> {
try {
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
@@ -6843,7 +6844,6 @@ public class PdfJsonConversionService {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
})
.start();
});
}
}
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.SignatureFile;
@@ -24,6 +22,8 @@ import stirling.software.SPDF.model.api.signature.SavedSignatureRequest;
import stirling.software.SPDF.model.api.signature.SavedSignatureResponse;
import stirling.software.common.configuration.InstallationPathConfig;
import tools.jackson.databind.ObjectMapper;
@Service
@Slf4j
public class SharedSignatureService {
@@ -33,9 +33,9 @@ public class SharedSignatureService {
private final String ALL_USERS_FOLDER = "ALL_USERS";
private final ObjectMapper objectMapper;
public SharedSignatureService() {
public SharedSignatureService(ObjectMapper objectMapper) {
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
this.objectMapper = new ObjectMapper();
this.objectMapper = objectMapper;
}
public boolean hasAccessToFile(String username, String fileName) throws IOException {
@@ -18,8 +18,6 @@ import org.apache.pdfbox.pdmodel.font.PDFont;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -38,6 +36,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.TaskManager;
import stirling.software.common.util.ExceptionUtils;
import tools.jackson.databind.ObjectMapper;
/**
* Service for lazy loading PDF pages. Caches PDF documents and extracts pages on-demand to reduce
* memory usage for large PDFs.
@@ -255,7 +255,8 @@ public class PdfLazyLoadingService {
/** Schedules automatic cleanup of cached documents after 30 minutes. */
private void scheduleDocumentCleanup(String jobId) {
new Thread(
Thread.ofVirtual()
.start(
() -> {
try {
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
@@ -264,8 +265,7 @@ public class PdfLazyLoadingService {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
})
.start();
});
}
/**
@@ -18,14 +18,14 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@Component
@RequiredArgsConstructor
@@ -202,20 +202,34 @@ public class Type3FontLibrary {
if (payload == null) {
return null;
}
byte[] data = null;
String base64;
if (payload.base64 != null && !payload.base64.isBlank()) {
// Validate the base64 string without wasteful full decode
// Only decode a small prefix to verify encoding is valid
try {
data = Base64.getDecoder().decode(payload.base64);
byte[] probe =
Base64.getDecoder()
.decode(
payload.base64.substring(
0, Math.min(4, payload.base64.length())));
if (probe.length == 0 && payload.base64.length() <= 4) {
return null;
}
} catch (IllegalArgumentException ex) {
log.warn("[TYPE3] Invalid base64 payload in Type3 library: {}", ex.getMessage());
return null;
}
// Keep the original base64 string directly avoids 3x memory pressure
base64 = payload.base64;
} else if (payload.resource != null && !payload.resource.isBlank()) {
data = loadResourceBytes(payload.resource);
}
if (data == null || data.length == 0) {
byte[] data = loadResourceBytes(payload.resource);
if (data == null || data.length == 0) {
return null;
}
base64 = Base64.getEncoder().encodeToString(data);
} else {
return null;
}
String base64 = Base64.getEncoder().encodeToString(data);
return new Type3FontLibraryPayload(base64, normalizeFormat(payload.format));
}
@@ -26,14 +26,15 @@ import org.apache.pdfbox.pdmodel.font.PDType3Font;
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
import stirling.software.SPDF.service.pdfjson.type3.Type3GlyphExtractor;
import stirling.software.SPDF.service.pdfjson.type3.model.Type3GlyphOutline;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectWriter;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
/**
* Small CLI helper that scans a PDF for Type3 fonts, computes their signatures, and optionally
* emits JSON describing the glyph coverage. This allows Type3 library entries to be added without
@@ -48,7 +49,7 @@ import stirling.software.SPDF.service.pdfjson.type3.model.Type3GlyphOutline;
public final class Type3SignatureTool {
private static final ObjectMapper OBJECT_MAPPER =
new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
private Type3SignatureTool() {}
@@ -97,7 +97,7 @@ public class SvgToPdf {
private GraphicsNode buildGvtWithTimeout(BridgeContext ctx, SVGDocument svgDoc)
throws IOException {
GVTBuilder builder = new GVTBuilder();
ExecutorService executor = Executors.newSingleThreadExecutor();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Callable<GraphicsNode> buildTask = () -> builder.build(ctx, svgDoc);
Future<GraphicsNode> future = executor.submit(buildTask);
@@ -13,6 +13,18 @@ logging.level.stirling.software.common.service.JobExecutorService=INFO
logging.level.stirling.software.common.service.TaskManager=INFO
spring.jpa.open-in-view=false
server.forward-headers-strategy=NATIVE
# Enable HTTP/2 for improved performance (multiplexed streams, header compression)
server.http2.enabled=true
# Enable virtual threads (Java 21+, pinning fix in Java 25)
spring.threads.virtual.enabled=true
# Response compression
server.compression.enabled=true
server.compression.min-response-size=1024
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript
server.error.path=/error
server.error.whitelabel.enabled=false
server.error.include-stacktrace=always
@@ -38,7 +50,7 @@ spring.devtools.livereload.enabled=true
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
server.tomcat.max-http-header-size=32768
server.jetty.max-http-request-header-size=32768
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL
spring.datasource.driver-class-name=org.h2.Driver
@@ -27,13 +27,13 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem;
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@ExtendWith(MockitoExtension.class)
class EditTableOfContentsControllerTest {
@@ -40,14 +40,15 @@ import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
import stirling.software.SPDF.service.VeraPDFService;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@DisplayName("GetInfoOnPDF Controller Tests")
@ExtendWith(MockitoExtension.class)
class GetInfoOnPDFTest {
@@ -64,7 +65,7 @@ class GetInfoOnPDFTest {
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
objectMapper = JsonMapper.builder().build();
}
/** Helper method to load a PDF file from test resources */
@@ -216,8 +217,8 @@ class GetInfoOnPDFTest {
Assertions.assertTrue(jsonNode.has("Permissions"));
JsonNode metadata = jsonNode.get("Metadata");
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
}
}
@@ -315,12 +316,12 @@ class GetInfoOnPDFTest {
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
JsonNode metadata = jsonNode.get("Metadata");
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText());
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText());
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText());
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText());
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText(""));
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText(""));
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText(""));
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText(""));
Assertions.assertTrue(metadata.has("CreationDate"));
Assertions.assertTrue(metadata.has("ModificationDate"));
@@ -512,10 +513,10 @@ class GetInfoOnPDFTest {
JsonNode page1 = perPageInfo.get("Page 1");
Assertions.assertTrue(page1.has("Size"));
Assertions.assertTrue(page1.get("Size").has("Standard Page"));
Assertions.assertEquals("A4", page1.get("Size").get("Standard Page").asText());
Assertions.assertEquals("A4", page1.get("Size").get("Standard Page").asText(""));
JsonNode page2 = perPageInfo.get("Page 2");
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText());
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText(""));
loadedDoc.close();
}
@@ -569,7 +570,8 @@ class GetInfoOnPDFTest {
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
Assertions.assertTrue(jsonNode.has("error"));
Assertions.assertTrue(jsonNode.get("error").asText().contains("PDF file is required"));
Assertions.assertTrue(
jsonNode.get("error").asText("").contains("PDF file is required"));
}
@Test
@@ -645,7 +647,7 @@ class GetInfoOnPDFTest {
Assertions.assertTrue(jsonNode.has("error"));
Assertions.assertTrue(
jsonNode.get("error").asText().contains("exceeds maximum allowed size"));
jsonNode.get("error").asText("").contains("exceeds maximum allowed size"));
}
}
@@ -7,14 +7,15 @@ import java.util.Map;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
class ApiEndpointTest {
private final ObjectMapper mapper = new ObjectMapper();
private final ObjectMapper mapper = JsonMapper.builder().build();
private JsonNode postNodeWithParams(String description, String... names) {
ObjectNode post = mapper.createObjectNode();
@@ -12,14 +12,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext;
import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.common.service.UserServiceInterface;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class ApiDocServiceTest {
@@ -27,11 +28,11 @@ class ApiDocServiceTest {
@Mock UserServiceInterface userService;
ApiDocService apiDocService;
ObjectMapper mapper = new ObjectMapper();
ObjectMapper mapper = JsonMapper.builder().build();
@BeforeEach
void setUp() {
apiDocService = new ApiDocService(servletContext, userService);
apiDocService = new ApiDocService(mapper, servletContext, userService);
}
private void setApiDocumentation(Map<String, ApiEndpoint> docs) throws Exception {
@@ -20,6 +20,8 @@ import org.mockito.MockedStatic;
import stirling.software.SPDF.model.SignatureFile;
import stirling.software.common.configuration.InstallationPathConfig;
import tools.jackson.databind.json.JsonMapper;
class SignatureServiceTest {
@TempDir Path tempDir;
@@ -53,7 +55,7 @@ class SignatureServiceTest {
.thenReturn(tempDir.toString());
// Initialize the service with our temp directory
signatureService = new SharedSignatureService();
signatureService = new SharedSignatureService(JsonMapper.builder().build());
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies {
api 'org.springframework.boot:spring-boot-starter-jetty'
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
api 'org.springframework.boot:spring-boot-starter-security-oauth2-client'
api 'org.springframework.boot:spring-boot-starter-mail'
api 'org.springframework.boot:spring-boot-starter-cache'
api 'com.github.ben-manes.caffeine:caffeine'
@@ -2,21 +2,22 @@ package stirling.software.proprietary.config;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.core.task.support.TaskExecutorAdapter;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* MDC context-propagating task decorator Copies MDC context from the caller thread to the async
* executor thread
* MDC context-propagating task decorator. Copies MDC context from the caller thread to the
* virtual thread executing the task.
*/
static class MDCContextTaskDecorator implements TaskDecorator {
@Override
@@ -42,16 +43,9 @@ public class AsyncConfig {
@Bean(name = "auditExecutor")
public Executor auditExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(2);
exec.setMaxPoolSize(8);
exec.setQueueCapacity(1_000);
exec.setThreadNamePrefix("audit-");
// Set the task decorator to propagate MDC context
exec.setTaskDecorator(new MDCContextTaskDecorator());
exec.initialize();
return exec;
TaskExecutorAdapter adapter =
new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
adapter.setTaskDecorator(new MDCContextTaskDecorator());
return adapter;
}
}
@@ -1,15 +1,12 @@
package stirling.software.proprietary.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/** Configuration to enable scheduling for the audit system. */
/** Configuration for audit system transaction management. */
@Configuration
@EnableTransactionManagement
@EnableScheduling
public class AuditJpaConfig {
// This configuration enables scheduling for audit cleanup tasks
// JPA repositories are now managed by DatabaseConfig to avoid conflicts
// No additional beans or methods needed
// Scheduling is enabled on SPDFApplication no duplicate @EnableScheduling needed.
// JPA repositories are now managed by DatabaseConfig to avoid conflicts.
}
@@ -12,8 +12,6 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -21,6 +19,8 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.util.SecretMasker;
import tools.jackson.databind.ObjectMapper;
@Component
@Primary
@RequiredArgsConstructor
@@ -68,7 +68,10 @@ public class CustomAuditEventRepository implements AuditEventRepository {
.build();
repo.save(ent);
} catch (Exception e) {
e.printStackTrace(); // fail-open
log.error(
"Failed to persist audit event (fail-open); principal={}",
ev.getPrincipal(),
e);
}
}
}
@@ -28,9 +28,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -47,6 +44,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/** REST endpoints for the audit dashboard. */
@Slf4j
@RestController
@@ -266,7 +266,7 @@ public class AuditDashboardController {
headers.setContentDispositionFormData("attachment", "audit_export.json");
return ResponseEntity.ok().headers(headers).body(jsonBytes);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.error("Error serializing audit events to JSON", e);
return ResponseEntity.internalServerError().build();
}
@@ -20,9 +20,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +29,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/** REST API controller for audit data used by React frontend. */
@Slf4j
@ProprietaryUiDataApi
@@ -332,7 +332,7 @@ public class AuditRestController {
@SuppressWarnings("unchecked")
Map<String, Object> parsed = objectMapper.readValue(event.getData(), Map.class);
details = parsed;
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.warn("Failed to parse audit event data as JSON: {}", event.getData());
details.put("rawData", event.getData());
}
@@ -380,7 +380,7 @@ public class AuditRestController {
headers.setContentDispositionFormData("attachment", "audit_export.json");
return ResponseEntity.ok().headers(headers).body(jsonBytes);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.error("Error serializing audit events to JSON", e);
return ResponseEntity.internalServerError().build();
}
@@ -15,9 +15,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Data;
@@ -54,6 +51,9 @@ import stirling.software.proprietary.security.service.TeamService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import stirling.software.proprietary.service.UserLicenseSettingsService;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@ProprietaryUiDataApi
public class ProprietaryUIDataController {
@@ -414,7 +414,7 @@ public class ProprietaryUIDataController {
String settingsJson;
try {
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.error("Error converting settings map", e);
return ResponseEntity.status(500).build();
}
@@ -8,9 +8,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -20,6 +17,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;
/** REST API controller for usage analytics data used by React frontend. */
@Slf4j
@ProprietaryUiDataApi
@@ -135,7 +135,7 @@ public class UsageRestController {
return normalizeEndpoint(requestUri.toString());
}
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
log.debug("Failed to parse audit data JSON: {}", dataJson, e);
}
@@ -6,9 +6,9 @@ import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -75,10 +75,18 @@ public class DatabaseConfig {
}
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
// Support AOT training: override URL via system property to avoid H2 file lock
// conflicts when the AOT RECORD phase starts a second Spring context
String overrideUrl = System.getProperty("stirling.datasource.url");
String url =
(overrideUrl != null && !overrideUrl.isBlank())
? overrideUrl
: DATASOURCE_DEFAULT_URL;
log.info("Using default H2 database");
dataSourceBuilder
.url(DATASOURCE_DEFAULT_URL)
.url(url)
.driverClassName(DatabaseDriver.H2.getDriverClassName())
.username(DEFAULT_USERNAME);
@@ -14,15 +14,17 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@@ -82,7 +84,7 @@ public class SecurityConfiguration {
private final PersistentLoginRepository persistentLoginRepository;
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
private final OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver;
private final stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService;
private final ClientRegistrationRepository clientRegistrationRepository;
@@ -105,7 +107,7 @@ public class SecurityConfiguration {
@Autowired(required = false)
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
@Autowired(required = false)
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver,
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository,
stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService) {
@@ -151,7 +153,8 @@ public class SecurityConfiguration {
// Default to allowing all origins when nothing is configured
cfg.setAllowedOriginPatterns(List.of("*"));
log.info(
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
"No CORS allowed origins configured in settings.yml"
+ " (system.corsAllowedOrigins); allowing all origins.");
}
// Explicitly configure supported HTTP methods (include OPTIONS for preflight)
@@ -225,7 +228,7 @@ public class SecurityConfiguration {
http.cors(cors -> cors.configurationSource(corsSource));
} else {
// Explicitly disable CORS when no origins are configured
http.cors(cors -> cors.disable());
http.cors(CorsConfigurer::disable);
}
http.csrf(CsrfConfigurer::disable);
@@ -233,24 +236,24 @@ public class SecurityConfiguration {
// Configure X-Frame-Options based on settings.yml configuration
// When login is disabled, automatically disable X-Frame-Options to allow embedding
if (!loginEnabledValue) {
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::disable));
} else {
String xFrameOption = securityProperties.getXFrameOptions();
if (xFrameOption != null) {
http.headers(
headers -> {
if ("DISABLED".equalsIgnoreCase(xFrameOption)) {
headers.frameOptions(frameOptions -> frameOptions.disable());
headers.frameOptions(FrameOptionsConfig::disable);
} else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) {
headers.frameOptions(frameOptions -> frameOptions.sameOrigin());
headers.frameOptions(FrameOptionsConfig::sameOrigin);
} else {
// Default to DENY
headers.frameOptions(frameOptions -> frameOptions.deny());
headers.frameOptions(FrameOptionsConfig::deny);
}
});
} else {
// If not configured, use default DENY
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.deny()));
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::deny));
}
}
@@ -381,8 +384,8 @@ public class SecurityConfiguration {
}
// Handle SAML
if (securityProperties.isSaml2Active() && runningProOrHigher) {
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
OpenSaml5AuthenticationProvider authenticationProvider =
new OpenSaml5AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider)
@@ -12,11 +12,6 @@ import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.posthog.java.shaded.org.json.JSONException;
import com.posthog.java.shaded.org.json.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -24,6 +19,9 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.RegexPatternUtils;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
@Service
@Slf4j
@RequiredArgsConstructor
@@ -47,7 +45,7 @@ public class KeygenLicenseVerifier {
private static final String JWT_PREFIX = "key/";
private static final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper;
private final ApplicationProperties applicationProperties;
// Shared HTTP client for connection pooling
@@ -135,11 +133,11 @@ public class KeygenLicenseVerifier {
String algorithm = "";
try {
JSONObject attrs = new JSONObject(payload);
encryptedData = (String) attrs.get("enc");
encodedSignature = (String) attrs.get("sig");
algorithm = (String) attrs.get("alg");
} catch (JSONException e) {
JsonNode attrs = objectMapper.readTree(payload);
encryptedData = attrs.path("enc").asText("");
encodedSignature = attrs.path("sig").asText("");
algorithm = attrs.path("alg").asText("");
} catch (Exception e) {
log.error("Failed to parse license file: {}", e.getMessage());
return false;
}
@@ -215,11 +213,17 @@ public class KeygenLicenseVerifier {
private boolean processCertificateData(String certData, LicenseContext context) {
try {
JSONObject licenseData = new JSONObject(certData);
JSONObject metaObj = licenseData.optJSONObject("meta");
if (metaObj != null) {
String issuedStr = metaObj.optString("issued", null);
String expiryStr = metaObj.optString("expiry", null);
JsonNode licenseData = objectMapper.readTree(certData);
JsonNode metaObj = licenseData.path("meta");
if (!metaObj.isMissingNode() && metaObj.isObject()) {
String issuedStr =
metaObj.path("issued").isNull()
? null
: metaObj.path("issued").asText(null);
String expiryStr =
metaObj.path("expiry").isNull()
? null
: metaObj.path("expiry").asText(null);
if (issuedStr != null && expiryStr != null) {
java.time.Instant issued = java.time.Instant.parse(issuedStr);
@@ -244,31 +248,32 @@ public class KeygenLicenseVerifier {
}
// Get the main license data
JSONObject dataObj = licenseData.optJSONObject("data");
if (dataObj == null) {
JsonNode dataObj = licenseData.path("data");
if (dataObj.isMissingNode() || !dataObj.isObject()) {
log.error("No data object found in certificate");
return false;
}
// Extract license or machine information
JSONObject attributesObj = dataObj.optJSONObject("attributes");
if (attributesObj != null) {
JsonNode attributesObj = dataObj.path("attributes");
if (!attributesObj.isMissingNode() && attributesObj.isObject()) {
log.info("Found attributes in certificate data");
// Check for floating license
context.isFloatingLicense = attributesObj.optBoolean("floating", false);
context.maxMachines = attributesObj.optInt("maxMachines", 1);
context.isFloatingLicense = attributesObj.path("floating").asBoolean(false);
context.maxMachines = attributesObj.path("maxMachines").asInt(1);
// Extract metadata
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
if (metadataObj != null) {
JsonNode metadataObj = attributesObj.path("metadata");
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
// Check if this is an old license (no planType) with isEnterprise flag
context.isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false);
context.isEnterpriseLicense = metadataObj.path("isEnterprise").asBoolean(false);
// Extract user count - default based on license type
// Old licenses: Only had isEnterprise flag
// New licenses: Have planType field with "server" or "enterprise"
int users = metadataObj.optInt("users", context.isEnterpriseLicense ? 1 : 0);
int users =
metadataObj.path("users").asInt(context.isEnterpriseLicense ? 1 : 0);
// SERVER license (isEnterprise=false, users=0) = unlimited
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
@@ -282,7 +287,7 @@ public class KeygenLicenseVerifier {
}
// Check license status if available
String status = attributesObj.optString("status", null);
String status = attributesObj.path("status").asText(null);
if (status != null
&& !"ACTIVE".equals(status)
&& !"EXPIRING".equals(status)) { // Accept "EXPIRING" status as valid
@@ -372,11 +377,11 @@ public class KeygenLicenseVerifier {
try {
log.info("Processing license payload: {}", payload);
JSONObject licenseData = new JSONObject(payload);
JsonNode licenseData = objectMapper.readTree(payload);
JSONObject licenseObj = licenseData.optJSONObject("license");
if (licenseObj == null) {
String id = licenseData.optString("id", null);
JsonNode licenseObj = licenseData.path("license");
if (licenseObj.isMissingNode() || !licenseObj.isObject()) {
String id = licenseData.path("id").asText(null);
if (id != null) {
log.info("Found license ID: {}", id);
licenseObj = licenseData; // Use the root object as the license object
@@ -386,18 +391,18 @@ public class KeygenLicenseVerifier {
}
}
String licenseId = licenseObj.optString("id", "unknown");
String licenseId = licenseObj.path("id").asText("unknown");
log.info("Processing license with ID: {}", licenseId);
// Check for floating license in license object
context.isFloatingLicense = licenseObj.optBoolean("floating", false);
context.maxMachines = licenseObj.optInt("maxMachines", 1);
context.isFloatingLicense = licenseObj.path("floating").asBoolean(false);
context.maxMachines = licenseObj.path("maxMachines").asInt(1);
if (context.isFloatingLicense) {
log.info("Detected floating license with max machines: {}", context.maxMachines);
}
// Check expiry date
String expiryStr = licenseObj.optString("expiry", null);
String expiryStr = licenseObj.path("expiry").asText(null);
if (expiryStr != null && !"null".equals(expiryStr)) {
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
java.time.Instant now = java.time.Instant.now();
@@ -413,9 +418,9 @@ public class KeygenLicenseVerifier {
}
// Extract account, product, policy info
JSONObject accountObj = licenseData.optJSONObject("account");
if (accountObj != null) {
String accountId = accountObj.optString("id", "unknown");
JsonNode accountObj = licenseData.path("account");
if (!accountObj.isMissingNode() && accountObj.isObject()) {
String accountId = accountObj.path("id").asText("unknown");
log.info("License belongs to account: {}", accountId);
// Verify this matches your expected account ID
@@ -426,14 +431,14 @@ public class KeygenLicenseVerifier {
}
// Extract policy information if available
JSONObject policyObj = licenseData.optJSONObject("policy");
if (policyObj != null) {
String policyId = policyObj.optString("id", "unknown");
JsonNode policyObj = licenseData.path("policy");
if (!policyObj.isMissingNode() && policyObj.isObject()) {
String policyId = policyObj.path("id").asText("unknown");
log.info("License uses policy: {}", policyId);
// Check for floating license in policy
boolean policyFloating = policyObj.optBoolean("floating", false);
int policyMaxMachines = policyObj.optInt("maxMachines", 1);
boolean policyFloating = policyObj.path("floating").asBoolean(false);
int policyMaxMachines = policyObj.path("maxMachines").asInt(1);
// Policy settings take precedence
if (policyFloating) {
@@ -445,16 +450,21 @@ public class KeygenLicenseVerifier {
}
// Extract max users and isEnterprise from policy or metadata
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
int users = policyObj.optInt("users", -1);
context.isEnterpriseLicense = policyObj.path("isEnterprise").asBoolean(false);
int users = policyObj.path("users").asInt(-1);
if (users == -1) {
// Try to get users from metadata if not at policy level
Object metadataObj = policyObj.opt("metadata");
if (metadataObj instanceof JSONObject metadata) {
JsonNode metadataObj = policyObj.path("metadata");
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
context.isEnterpriseLicense =
metadata.optBoolean("isEnterprise", context.isEnterpriseLicense);
users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0);
metadataObj
.path("isEnterprise")
.asBoolean(context.isEnterpriseLicense);
users =
metadataObj
.path("users")
.asInt(context.isEnterpriseLicense ? 1 : 0);
} else {
// Default based on license type
users = context.isEnterpriseLicense ? 1 : 0;
@@ -493,9 +503,9 @@ public class KeygenLicenseVerifier {
validateLicense(licenseKey, machineFingerprint, context);
if (validationResponse != null) {
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
String licenseId = validationResponse.path("data").path("id").asText();
String licenseId = validationResponse.path("data").path("id").asText("");
if (!isValid) {
String code = validationResponse.path("meta").path("code").asText();
String code = validationResponse.path("meta").path("code").asText("");
log.info(code);
if ("NO_MACHINE".equals(code)
|| "NO_MACHINES".equals(code)
@@ -579,8 +589,8 @@ public class KeygenLicenseVerifier {
JsonNode metaNode = jsonResponse.path("meta");
boolean isValid = metaNode.path("valid").asBoolean();
String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText();
String detail = metaNode.path("detail").asText("");
String code = metaNode.path("code").asText("");
log.info("License validity: {}", isValid);
log.info("Validation detail: {}", detail);
@@ -604,7 +614,7 @@ public class KeygenLicenseVerifier {
if (includedNode.isArray()) {
for (JsonNode node : includedNode) {
if ("policies".equals(node.path("type").asText())) {
if ("policies".equals(node.path("type").asText(""))) {
policyNode = node;
break;
}
@@ -690,9 +700,9 @@ public class KeygenLicenseVerifier {
for (JsonNode machine : machines) {
if (machineFingerprint.equals(
machine.path("attributes").path("fingerprint").asText())) {
machine.path("attributes").path("fingerprint").asText(""))) {
isCurrentMachineActivated = true;
currentMachineId = machine.path("id").asText();
currentMachineId = machine.path("id").asText("");
log.info(
"Current machine is already activated with ID: {}",
currentMachineId);
@@ -726,7 +736,7 @@ public class KeygenLicenseVerifier {
java.time.Instant.parse(createdStr);
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
oldestTime = createdTime;
oldestMachineId = machine.path("id").asText();
oldestMachineId = machine.path("id").asText("");
}
} catch (Exception e) {
log.warn(
@@ -740,7 +750,7 @@ public class KeygenLicenseVerifier {
if (oldestMachineId == null) {
log.warn(
"Could not determine oldest machine by timestamp, using first machine in list");
oldestMachineId = machines.path(0).path("id").asText();
oldestMachineId = machines.path(0).path("id").asText("");
}
log.info("Deregistering machine with ID: {}", oldestMachineId);
@@ -770,35 +780,28 @@ public class KeygenLicenseVerifier {
hostname = "Unknown";
}
JSONObject body =
new JSONObject()
.put(
"data",
new JSONObject()
.put("type", "machines")
.put(
"attributes",
new JSONObject()
.put("fingerprint", machineFingerprint)
.put(
"platform",
System.getProperty("os.name"))
.put("name", hostname))
.put(
"relationships",
new JSONObject()
.put(
"license",
new JSONObject()
.put(
"data",
new JSONObject()
.put(
"type",
"licenses")
.put(
"id",
licenseId)))));
tools.jackson.databind.node.ObjectNode attributes = objectMapper.createObjectNode();
attributes.put("fingerprint", machineFingerprint);
attributes.put("platform", System.getProperty("os.name"));
attributes.put("name", hostname);
tools.jackson.databind.node.ObjectNode licenseRef = objectMapper.createObjectNode();
licenseRef.put("type", "licenses");
licenseRef.put("id", licenseId);
tools.jackson.databind.node.ObjectNode licenseRelation = objectMapper.createObjectNode();
licenseRelation.set("data", licenseRef);
tools.jackson.databind.node.ObjectNode relationships = objectMapper.createObjectNode();
relationships.set("license", licenseRelation);
tools.jackson.databind.node.ObjectNode data = objectMapper.createObjectNode();
data.put("type", "machines");
data.set("attributes", attributes);
data.set("relationships", relationships);
tools.jackson.databind.node.ObjectNode body = objectMapper.createObjectNode();
body.set("data", data);
HttpRequest request =
HttpRequest.newBuilder()
@@ -27,9 +27,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.HtmlUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -49,6 +46,9 @@ import stirling.software.proprietary.security.model.api.admin.SettingValueRespon
import stirling.software.proprietary.security.model.api.admin.UpdateSettingValueRequest;
import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequest;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@AdminApi
@RequiredArgsConstructor
@PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -543,7 +543,8 @@ public class AdminSettingsController {
pendingChanges.clear();
// Give the HTTP response time to complete, then exit
new Thread(
Thread.ofVirtual()
.start(
() -> {
try {
Thread.sleep(1000);
@@ -554,8 +555,7 @@ public class AdminSettingsController {
log.error("Restart interrupted: {}", e.getMessage(), e);
Thread.currentThread().interrupt();
}
})
.start();
});
return ResponseEntity.ok(
Map.of(
@@ -20,9 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Data;
@@ -31,6 +28,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.RuntimePathConfig;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@RestController
@RequestMapping("/api/v1/ui-data")
@@ -39,6 +39,7 @@ public class UIDataTessdataController {
private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]");
private final RuntimePathConfig runtimePathConfig;
private final ObjectMapper objectMapper;
private static volatile List<String> cachedRemoteTessdata = null;
private static volatile long cachedRemoteTessdataExpiry = 0L;
private static final long REMOTE_TESSDATA_TTL_MS = 10 * 60 * 1000; // 10 minutes
@@ -223,9 +224,9 @@ public class UIDataTessdataController {
}
try (InputStream is = connection.getInputStream()) {
ObjectMapper mapper = new ObjectMapper();
List<Map<String, Object>> items =
mapper.readValue(is, new TypeReference<List<Map<String, Object>>>() {});
objectMapper.readValue(
is, new TypeReference<List<Map<String, Object>>>() {});
List<String> languages =
items.stream()
.map(item -> (String) item.get("name"))
@@ -11,7 +11,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
private Object credentials;
public ApiKeyAuthenticationToken(String apiKey) {
super(null);
super((Collection<? extends GrantedAuthority>) null);
this.principal = null;
this.credentials = apiKey;
setAuthenticated(false);
@@ -14,7 +14,7 @@ import org.opensaml.saml.saml2.core.AuthnStatement;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import lombok.RequiredArgsConstructor;
@@ -15,7 +15,7 @@ import org.springframework.security.saml2.provider.service.registration.InMemory
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest;
@@ -151,10 +151,10 @@ public class Saml2Configuration {
@Bean
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
public OpenSaml5AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
OpenSaml5AuthenticationRequestResolver resolver =
new OpenSaml5AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setRelayStateResolver(
request -> {
@@ -20,9 +20,6 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
@@ -40,21 +37,25 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@Slf4j
@Service
public class JwtService implements JwtServiceInterface {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final ObjectMapper objectMapper;
private final KeyPersistenceServiceInterface keyPersistenceService;
private final boolean v2Enabled;
private final ApplicationProperties.Security securityProperties;
@Autowired
public JwtService(
ObjectMapper objectMapper,
@Qualifier("v2Enabled") boolean v2Enabled,
KeyPersistenceServiceInterface keyPersistenceService,
ApplicationProperties applicationProperties) {
this.objectMapper = objectMapper;
this.v2Enabled = v2Enabled;
this.keyPersistenceService = keyPersistenceService;
this.securityProperties = applicationProperties.getSecurity();
@@ -374,14 +375,14 @@ public class JwtService implements JwtServiceInterface {
byte[] headerBytes = Base64.getUrlDecoder().decode(tokenParts[0]);
Map<String, Object> header =
OBJECT_MAPPER.readValue(
objectMapper.readValue(
headerBytes, new TypeReference<Map<String, Object>>() {});
Object keyId = header.get("kid");
return keyId instanceof String ? (String) keyId : null;
} catch (IllegalArgumentException e) {
log.debug("Failed to decode Base64 JWT header: {}", e.getMessage());
return null;
} catch (java.io.IOException e) {
} catch (tools.jackson.core.JacksonException e) {
log.debug("Failed to parse JWT header as JSON: {}", e.getMessage());
return null;
}
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
@@ -24,6 +22,8 @@ import stirling.software.common.service.PersonalSignatureServiceInterface;
import stirling.software.proprietary.model.api.signature.SavedSignatureRequest;
import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
import tools.jackson.databind.ObjectMapper;
/**
* Service for managing user signatures with authentication and storage limits. This proprietary
* version enforces per-user quotas and requires authentication. Provides access to personal
@@ -36,15 +36,16 @@ public class SignatureService implements PersonalSignatureServiceInterface {
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
private final String SIGNATURE_BASE_PATH;
private final String ALL_USERS_FOLDER = "ALL_USERS";
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper;
// Storage limits per user
private static final int MAX_SIGNATURES_PER_USER = 20;
private static final long MAX_SIGNATURE_SIZE_BYTES = 2_000_000; // 2MB per signature
private static final long MAX_TOTAL_USER_STORAGE_BYTES = 20_000_000; // 20MB total per user
public SignatureService() {
public SignatureService(ObjectMapper objectMapper) {
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
this.objectMapper = objectMapper;
}
/**
@@ -16,8 +16,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.config.AuditConfigurationProperties;
@@ -35,6 +33,9 @@ import stirling.software.proprietary.security.service.MfaService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import stirling.software.proprietary.service.UserLicenseSettingsService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class ProprietaryUIDataControllerTest {
@@ -63,7 +64,7 @@ class ProprietaryUIDataControllerTest {
applicationProperties.getSecurity().getSaml2().setEnabled(false);
auditConfig = new AuditConfigurationProperties(applicationProperties);
objectMapper = new ObjectMapper();
objectMapper = JsonMapper.builder().build();
controller =
new ProprietaryUIDataController(
@@ -26,8 +26,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.security.model.AuthenticationType;
@@ -42,10 +40,13 @@ import stirling.software.proprietary.security.service.RefreshRateLimitService;
import stirling.software.proprietary.security.service.TotpService;
import stirling.software.proprietary.security.service.UserService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class AuthControllerLoginTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
private MockMvc mockMvc;
private ApplicationProperties.Security securityProperties;
@@ -26,8 +26,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.CustomUserDetailsService;
@@ -37,12 +35,15 @@ import stirling.software.proprietary.security.service.MfaService;
import stirling.software.proprietary.security.service.TotpService;
import stirling.software.proprietary.security.service.UserService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class AuthControllerMfaTest {
private static final String USERNAME = "[email protected]";
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
private MockMvc mockMvc;
private Authentication authentication;
@@ -19,6 +19,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import stirling.software.common.configuration.RuntimePathConfig;
import tools.jackson.databind.json.JsonMapper;
class UIDataTessdataControllerTest {
@Test
@@ -27,7 +29,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn("ignored/path");
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -49,7 +51,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -77,7 +79,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -100,7 +102,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng", "fra");
@@ -139,7 +141,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -164,7 +166,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected boolean isWritableDirectory(Path dir) {
return false;
@@ -186,7 +188,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -217,7 +219,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -258,7 +260,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng", "fra");
@@ -282,7 +284,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -307,7 +309,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -330,7 +332,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(missingDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected List<String> getRemoteTessdataLanguages() {
return List.of("eng");
@@ -352,7 +354,7 @@ class UIDataTessdataControllerTest {
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
UIDataTessdataController controller =
new UIDataTessdataController(runtimePathConfig) {
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
@Override
protected boolean isWritableDirectory(Path dir) {
return false;
@@ -21,8 +21,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.UserRepository;
@@ -35,10 +33,13 @@ import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import stirling.software.proprietary.service.UserLicenseSettingsService;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class UserControllerTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = JsonMapper.builder().build();
@Mock private UserService userService;
@Mock private SessionPersistentRegistry sessionRegistry;
@@ -36,6 +36,9 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
class JwtServiceTest {
@@ -66,7 +69,8 @@ class JwtServiceTest {
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
ApplicationProperties applicationProperties = new ApplicationProperties();
jwtService = new JwtService(true, keystoreService, applicationProperties);
ObjectMapper objectMapper = JsonMapper.builder().build();
jwtService = new JwtService(objectMapper, true, keystoreService, applicationProperties);
}
@Test