Update SaaS to latest main (#6667)

# Description of Changes
> [!warning]
> **Do not** squash this on merge. It should be merged via a merge
commit

Fixes conflicts in `pgvector_store.py`. 

Also since codespell is failing, add comments to ignore the errors in
`sync_en_us_spelling.py`

---------

Co-authored-by: Ludy <[email protected]>
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
James Brunton
2026-06-15 14:53:22 +01:00
committed by GitHub
co-authored by Ludy Anthony Stirling
79 changed files with 379 additions and 346 deletions
+13 -1
View File
@@ -1,6 +1,8 @@
build: &build
- build.gradle
- app/(common|core|proprietary)/build.gradle
- Taskfile.yml
- .taskfiles/backend.yml
openapi: &openapi
- *build
@@ -38,6 +40,9 @@ project: &project
- frontend/**
- docker/**
- scripts/RestartHelper.java
- Taskfile.yml
- .taskfiles/backend.yml
- .taskfiles/docker.yml
- scripts/db-migration/**
- .github/workflows/db-migration-test.yml
@@ -55,6 +60,9 @@ frontend: &frontend
- scripts/summarize_type3_signatures.py
- scripts/type3_to_cff.py
- scripts/update_type3_library.py
- Taskfile.yml
- .taskfiles/frontend.yml
- .taskfiles/e2e.yml
# Files that affect the Tauri desktop bundle. Gate the multi-OS Tauri build
# job on changes to any of these.
@@ -66,6 +74,8 @@ tauri: &tauri
- frontend/package-lock.json
- frontend/editor/vite.config.ts
- .github/workflows/tauri-build.yml
- Taskfile.yml
- .taskfiles/desktop.yml
# Files that affect the AI engine (Python tool models, fixers, tests). Gate
# the engine validation job on changes to engine sources or to the Java
@@ -74,6 +84,8 @@ engine: &engine
- engine/**
- app/(common|core|proprietary)/src/main/java/**
- .github/workflows/ai-engine.yml
- Taskfile.yml
- .taskfiles/engine.yml
licenses-frontend: &licenses-frontend
- ".github/workflows/frontend-backend-licenses-update.yml"
@@ -102,4 +114,4 @@ proprietary: &proprietary
- configs/settings.yml.template
- build.gradle
- app/proprietary/build.gradle
- .github/workflows/build-enterprise.yml
- .github/workflows/build-enterprise.yml
@@ -222,7 +222,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
cache-disabled: true
- name: Install Task
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
cache-disabled: true
- name: Install Task
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
cache-disabled: true
- name: Install Task
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
cache-disabled: true
# No `-PnoSpotless` here yet because the upstream cache layer matches the
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
cache-disabled: true
- name: Set up Docker Buildx
@@ -349,7 +349,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
+3 -3
View File
@@ -73,7 +73,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
@@ -148,7 +148,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Setup Node.js
if: matrix.variant.build_frontend == true
@@ -252,7 +252,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
+1 -1
View File
@@ -78,7 +78,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Set up Docker Buildx
id: buildx
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Generate Swagger documentation
run: ./gradlew :stirling-pdf:generateOpenApiDocs
+1 -1
View File
@@ -136,7 +136,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Setup Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
+1 -1
View File
@@ -106,7 +106,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
cache-disabled: true
- name: Install Task
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
with:
gradle-version: 9.3.1
gradle-version: 9.5.1
- name: Build with Gradle
run: ./gradlew build
+9 -8
View File
@@ -127,14 +127,15 @@ tasks:
cmds:
- rm -rf runtime/jre
- mkdir -p runtime
- >-
jlink
--add-modules {{.JLINK_MODULES}}
--strip-debug
--compress=zip-6
--no-header-files
--no-man-pages
--output runtime/jre
- |
JLINK_COMPRESS="$(jlink --help 2>&1 | grep -q 'zip-\[0-9\]' && echo zip-6 || echo 2)"
jlink \
--add-modules {{.JLINK_MODULES}} \
--strip-debug \
--compress="$JLINK_COMPRESS" \
--no-header-files \
--no-man-pages \
--output runtime/jre
# jlink emits its files mode 444 (read-only). Tauri's build-script
# resource copier preserves source permissions when staging
# `runtime/jre/**/*` into `target/<profile>/runtime/jre/...`, so the
@@ -3,7 +3,6 @@ package stirling.software.common.configuration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
@@ -122,17 +121,17 @@ public class AppConfig {
@Bean(name = "RunningInDocker")
public boolean runningInDocker() {
return Files.exists(Paths.get("/.dockerenv"));
return Files.exists(Path.of("/.dockerenv"));
}
@Bean(name = "configDirMounted")
public boolean isRunningInDockerWithConfig() {
Path dockerEnv = Paths.get("/.dockerenv");
Path dockerEnv = Path.of("/.dockerenv");
// default to true if not docker
if (!Files.exists(dockerEnv)) {
return true;
}
Path mountInfo = Paths.get("/proc/1/mountinfo");
Path mountInfo = Path.of("/proc/1/mountinfo");
// this should always exist, if not some unknown usecase
if (!Files.exists(mountInfo)) {
return true;
@@ -7,7 +7,6 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
@@ -27,7 +26,7 @@ public class ConfigInitializer {
public void ensureConfigExists() throws IOException, URISyntaxException {
// 1) If settings file doesn't exist, create from template
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
Path destPath = Path.of(InstallationPathConfig.getSettingsPath());
boolean settingsFileExists = Files.exists(destPath);
@@ -39,7 +38,7 @@ public class ConfigInitializer {
if (settingsFileExists) {
// move settings.yml to settings.yml.{timestamp}.bak
Path backupPath =
Paths.get(
Path.of(
InstallationPathConfig.getSettingsPath()
+ "."
+ System.currentTimeMillis()
@@ -96,7 +95,7 @@ public class ConfigInitializer {
}
// 3) Ensure custom settings file exists
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
Path customSettingsPath = Path.of(InstallationPathConfig.getCustomSettingsPath());
if (Files.notExists(customSettingsPath)) {
Files.createFile(customSettingsPath);
log.info("Created custom_settings file: {}", customSettingsPath);
@@ -3,7 +3,6 @@ package stirling.software.common.configuration;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
@@ -201,7 +200,7 @@ public class RuntimePathConfig {
try {
// Normalize to absolute path
Path path = Paths.get(pathStr.trim()).toAbsolutePath().normalize();
Path path = Path.of(pathStr.trim()).toAbsolutePath().normalize();
String normalizedPath = path.toString();
// Check for duplicates
@@ -224,9 +223,9 @@ public class RuntimePathConfig {
private void detectOverlappingPaths(List<String> paths) {
for (int i = 0; i < paths.size(); i++) {
Path path1 = Paths.get(paths.get(i));
Path path1 = Path.of(paths.get(i));
for (int j = i + 1; j < paths.size(); j++) {
Path path2 = Paths.get(paths.get(j));
Path path2 = Path.of(paths.get(j));
// Check if one path is a parent of the other
if (path1.startsWith(path2)) {
@@ -246,10 +245,10 @@ public class RuntimePathConfig {
private void validatePipelinePaths() {
try {
Path finishedPath = Paths.get(pipelineFinishedFoldersPath).toAbsolutePath().normalize();
Path finishedPath = Path.of(pipelineFinishedFoldersPath).toAbsolutePath().normalize();
for (String watchedPathStr : pipelineWatchedFoldersPaths) {
Path watchedPath = Paths.get(watchedPathStr).toAbsolutePath().normalize();
Path watchedPath = Path.of(watchedPathStr).toAbsolutePath().normalize();
// Check if watched folder is same as finished folder
if (watchedPath.equals(finishedPath)) {
@@ -1,7 +1,6 @@
package stirling.software.common.model;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
@@ -24,7 +23,7 @@ public class FileInfo {
// Converts the file path string to a Path object.
public Path getFilePathAsPath() {
return Paths.get(filePath);
return Path.of(filePath);
}
// Formats the file size into a human-readable string.
@@ -3,7 +3,6 @@ package stirling.software.common.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -35,7 +34,7 @@ public class MobileScannerService {
public MobileScannerService() throws IOException {
// Create temp directory for mobile scanner uploads
this.tempDirectory =
Paths.get(System.getProperty("java.io.tmpdir"), "stirling-mobile-scanner");
Path.of(System.getProperty("java.io.tmpdir"), "stirling-mobile-scanner");
Files.createDirectories(tempDirectory);
log.info("Mobile scanner temp directory: {}", tempDirectory);
}
@@ -8,7 +8,7 @@ import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.lang.management.ThreadMXBean;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -160,7 +160,7 @@ public class PostHogService {
}
private boolean isRunningInDocker() {
return Files.exists(Paths.get("/.dockerenv"));
return Files.exists(Path.of("/.dockerenv"));
}
private Map<String, Object> getDockerMetrics() {
@@ -255,7 +255,7 @@ public class GeneralUtils {
String pattern = locationPattern;
if (pattern.startsWith("file:")) {
String rawPath = pattern.substring(5).replace("\\*", "").replace("/*", "");
Path normalizePath = Paths.get(rawPath).normalize();
Path normalizePath = Path.of(rawPath).normalize();
pattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*";
}
return ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
@@ -837,7 +837,7 @@ public class GeneralUtils {
}
public boolean createDir(String path) {
Path folder = Paths.get(path);
Path folder = Path.of(path);
if (!Files.exists(folder)) {
try {
Files.createDirectories(folder);
@@ -867,7 +867,7 @@ public class GeneralUtils {
public void saveKeyToSettings(String key, Object newValue) throws IOException {
String[] keyArray = key.split("\\.");
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
Path settingsPath = Path.of(InstallationPathConfig.getSettingsPath());
YamlHelper settingsYaml = new YamlHelper(settingsPath);
settingsYaml.updateValue(Arrays.asList(keyArray), newValue);
settingsYaml.saveOverride(settingsPath);
@@ -888,7 +888,7 @@ public class GeneralUtils {
return;
}
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
Path settingsPath = Path.of(InstallationPathConfig.getSettingsPath());
YamlHelper settingsYaml = new YamlHelper(settingsPath);
// Apply all updates to the same YamlHelper instance
@@ -974,11 +974,11 @@ public class GeneralUtils {
*/
public void extractPipeline() throws IOException {
Path pipelineDir =
Paths.get(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
Path.of(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
Files.createDirectories(pipelineDir);
for (String name : DEFAULT_VALID_PIPELINE) {
if (!Paths.get(name).getFileName().toString().equals(name)) {
if (!Path.of(name).getFileName().toString().equals(name)) {
log.error("Invalid pipeline file name: {}", name);
throw new IllegalArgumentException("Invalid pipeline file name: " + name);
}
@@ -1014,7 +1014,7 @@ public class GeneralUtils {
throw new IllegalArgumentException(
"scriptName must not contain path traversal characters");
}
if (!Paths.get(scriptName).getFileName().toString().equals(scriptName)) {
if (!Path.of(scriptName).getFileName().toString().equals(scriptName)) {
throw new IllegalArgumentException(
"scriptName must not contain path traversal characters");
}
@@ -1024,7 +1024,7 @@ public class GeneralUtils {
"scriptName must be either 'png_to_webp.py' or 'split_photos.py'");
}
Path scriptsDir = Paths.get(InstallationPathConfig.getScriptsPath(), PYTHON_SCRIPTS_DIR);
Path scriptsDir = Path.of(InstallationPathConfig.getScriptsPath(), PYTHON_SCRIPTS_DIR);
Files.createDirectories(scriptsDir);
Path target = scriptsDir.resolve(scriptName);
@@ -4,7 +4,6 @@ import java.io.File;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import lombok.extern.slf4j.Slf4j;
@@ -20,7 +19,7 @@ public class JarPathUtil {
public static Path currentJar() {
try {
Path jar =
Paths.get(
Path.of(
JarPathUtil.class
.getProtectionDomain()
.getCodeSource()
@@ -61,14 +60,14 @@ public class JarPathUtil {
}
// Location 2: ./build/libs/ (development build)
possibleLocations[1] = Paths.get("build", "libs", "restart-helper.jar").toAbsolutePath();
possibleLocations[1] = Path.of("build", "libs", "restart-helper.jar").toAbsolutePath();
// Location 3: app/common/build/libs/ (multi-module build)
possibleLocations[2] =
Paths.get("app", "common", "build", "libs", "restart-helper.jar").toAbsolutePath();
Path.of("app", "common", "build", "libs", "restart-helper.jar").toAbsolutePath();
// Location 4: Current working directory
possibleLocations[3] = Paths.get("restart-helper.jar").toAbsolutePath();
possibleLocations[3] = Path.of("restart-helper.jar").toAbsolutePath();
// Check each location
for (Path location : possibleLocations) {
@@ -2,7 +2,7 @@ package stirling.software.common.model;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -49,16 +49,15 @@ class ApplicationPropertiesLogicTest {
@Test
void tempFileManagement_defaults_and_overrides() {
Function<String, String> normalize = s -> Paths.get(s).normalize().toString();
Function<String, String> normalize = s -> Path.of(s).normalize().toString();
ApplicationProperties.TempFileManagement tfm =
new ApplicationProperties.TempFileManagement();
String expectedBase =
Paths.get(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf")
.toString();
Path.of(java.lang.System.getProperty("java.io.tmpdir"), "stirling-pdf").toString();
assertEquals(expectedBase, tfm.getBaseTmpDir());
String expectedLibre = Paths.get(expectedBase, "libreoffice").toString();
String expectedLibre = Path.of(expectedBase, "libreoffice").toString();
assertEquals(expectedLibre, tfm.getLibreofficeDir());
tfm.setBaseTmpDir("/custom/base");
@@ -4,7 +4,6 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -75,7 +74,7 @@ public class SPDFApplication {
Map<String, String> propertyFiles = new HashMap<>();
// External config files
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
Path settingsPath = Path.of(InstallationPathConfig.getSettingsPath());
log.info("Settings file: {}", settingsPath.toString());
if (Files.exists(settingsPath)) {
propertyFiles.put(
@@ -84,7 +83,7 @@ public class SPDFApplication {
log.warn("External configuration file '{}' does not exist.", settingsPath.toString());
}
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
Path customSettingsPath = Path.of(InstallationPathConfig.getCustomSettingsPath());
log.info("Custom settings file: {}", customSettingsPath.toString());
if (Files.exists(customSettingsPath)) {
String existingLocation =
@@ -95,7 +95,7 @@ public class ExternalAppDepConfig {
checkDependencyAndDisableGroup(cmd);
return null;
})
.collect(Collectors.toList());
.toList();
invokeAllWithTimeout(tasks, DEFAULT_TIMEOUT.plusSeconds(3));
// Python / OpenCV special handling
@@ -5,7 +5,6 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
@@ -115,7 +114,7 @@ public class UIDataController {
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
try (Stream<Path> paths =
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
Files.walk(Path.of(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
List<Path> jsonFiles =
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
@@ -9,7 +9,6 @@ import java.awt.print.PrinterJob;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Locale;
@@ -54,7 +53,7 @@ public class PrintFileController {
MultipartFile file = request.getFileInput();
String originalFilename = file.getOriginalFilename();
if (originalFilename != null
&& (originalFilename.contains("..") || Paths.get(originalFilename).isAbsolute())) {
&& (originalFilename.contains("..") || Path.of(originalFilename).isAbsolute())) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalid.filepath", "Invalid file path detected: " + originalFilename);
}
@@ -7,7 +7,6 @@ import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
@@ -82,7 +81,7 @@ public class PipelineDirectoryProcessor {
try {
for (String watchedFoldersDir : watchedFoldersDirs) {
scanWatchedFolder(Paths.get(watchedFoldersDir).toAbsolutePath());
scanWatchedFolder(Path.of(watchedFoldersDir).toAbsolutePath());
}
} finally {
// Clean up ThreadLocal to prevent memory leaks
@@ -442,7 +441,7 @@ public class PipelineDirectoryProcessor {
.replace("{outputFolder}", finishedFoldersDir)
.replace("{folderName}", dir.toString()))
.replaceAll("");
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
return Path.of(outputDir).isAbsolute() ? Path.of(outputDir) : Path.of(".", outputDir);
}
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir)
@@ -5,7 +5,6 @@ import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -326,12 +325,12 @@ public class PipelineProcessor {
}
List<Resource> outputFiles = new ArrayList<>();
for (File file : files) {
Path normalizedPath = Paths.get(file.getName()).normalize();
Path normalizedPath = Path.of(file.getName()).normalize();
if (normalizedPath.startsWith("..")) {
throw new SecurityException(
"Potential path traversal attempt in file name: " + file.getName());
}
Path path = Paths.get(file.getAbsolutePath());
Path path = Path.of(file.getAbsolutePath());
// debug statement
log.info("Reading file: {}", path);
if (Files.exists(path)) {
@@ -5,7 +5,6 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Value;
@@ -66,7 +65,7 @@ public class ReactRoutingController {
}
// Check for external index.html first (customFiles/static/)
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
Path externalIndexPath = Path.of(InstallationPathConfig.getStaticPath(), "index.html");
log.debug("Checking for custom index.html at: {}", externalIndexPath);
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
log.info("Using custom index.html from: {}", externalIndexPath);
@@ -136,7 +135,7 @@ public class ReactRoutingController {
private Resource getIndexHtmlResource() {
// Check external location first
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
Path externalIndexPath = Path.of(InstallationPathConfig.getStaticPath(), "index.html");
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
return new FileSystemResource(externalIndexPath.toFile());
}
@@ -588,7 +588,7 @@ public class PdfJsonConversionService {
.replaceAll("");
return String.format("%s (%s)", cleanName, subtype);
})
.collect(java.util.stream.Collectors.toList());
.toList();
long type3Fonts =
responseFonts.stream().filter(f -> "Type3".equals(f.getSubtype())).count();
@@ -1554,7 +1554,7 @@ public class PdfJsonConversionService {
.glyphName(outline.getGlyphName())
.unicode(outline.getUnicode())
.build())
.collect(Collectors.toList());
.toList();
}
} catch (Exception ex) {
log.debug(
@@ -4,7 +4,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Base64;
@@ -41,8 +40,8 @@ public class SharedSignatureService {
public boolean hasAccessToFile(String username, String fileName) throws IOException {
validateFileName(fileName);
// Check if file exists in user's personal folder or ALL_USERS folder
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
Path userPath = Path.of(SIGNATURE_BASE_PATH, username, fileName);
Path allUsersPath = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
return Files.exists(userPath) || Files.exists(allUsersPath);
}
@@ -52,7 +51,7 @@ public class SharedSignatureService {
// Get signatures from user's personal folder
if (StringUtils.hasText(username)) {
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
Path userFolder = Path.of(SIGNATURE_BASE_PATH, username);
if (Files.exists(userFolder)) {
try {
signatures.addAll(getSignaturesFromFolder(userFolder, "Personal"));
@@ -63,7 +62,7 @@ public class SharedSignatureService {
}
// Get signatures from ALL_USERS folder
Path allUsersFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
Path allUsersFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
if (Files.exists(allUsersFolder)) {
try {
signatures.addAll(getSignaturesFromFolder(allUsersFolder, "Shared"));
@@ -90,7 +89,7 @@ public class SharedSignatureService {
*/
public byte[] getSharedSignatureBytes(String fileName) throws IOException {
validateFileName(fileName);
Path allUsersPath = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
Path allUsersPath = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER, fileName);
if (!Files.exists(allUsersPath)) {
throw new FileNotFoundException("Shared signature file not found");
}
@@ -142,7 +141,7 @@ public class SharedSignatureService {
}
String folderName = "shared".equals(scope) ? ALL_USERS_FOLDER : username;
Path targetFolder = Paths.get(SIGNATURE_BASE_PATH, folderName);
Path targetFolder = Path.of(SIGNATURE_BASE_PATH, folderName);
Files.createDirectories(targetFolder);
long timestamp = System.currentTimeMillis();
@@ -193,7 +192,7 @@ public class SharedSignatureService {
List<SavedSignatureResponse> signatures = new ArrayList<>();
// Load personal signatures
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
if (Files.exists(personalFolder)) {
try (Stream<Path> stream = Files.list(personalFolder)) {
stream.filter(this::isImageFile)
@@ -224,7 +223,7 @@ public class SharedSignatureService {
}
// Load shared signatures
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
if (Files.exists(sharedFolder)) {
try (Stream<Path> stream = Files.list(sharedFolder)) {
stream.filter(this::isImageFile)
@@ -262,7 +261,7 @@ public class SharedSignatureService {
validateFileName(signatureId);
// Try to find and delete image file in personal folder
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
boolean deleted = false;
if (Files.exists(personalFolder)) {
@@ -283,7 +282,7 @@ public class SharedSignatureService {
// Try shared folder if not found in personal
if (!deleted) {
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
if (Files.exists(sharedFolder)) {
try (Stream<Path> stream = Files.list(sharedFolder)) {
List<Path> matchingFiles =
@@ -10,7 +10,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.font.PDType3Font;
@@ -268,7 +267,7 @@ public class Type3FontLibrary {
.filter(Objects::nonNull)
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
.toList();
}
private String normalizeAlias(String alias) {
@@ -3,7 +3,6 @@ package stirling.software.SPDF.service.pdfjson.type3.tool;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
@@ -285,9 +284,9 @@ public final class Type3SignatureTool {
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("--pdf".equals(arg) && i + 1 < args.length) {
pdf = Paths.get(args[++i]);
pdf = Path.of(args[++i]);
} else if ("--output".equals(arg) && i + 1 < args.length) {
output = Paths.get(args[++i]);
output = Path.of(args[++i]);
} else if ("--pretty".equals(arg)) {
pretty = true;
} else if ("--help".equals(arg) || "-h".equals(arg)) {
@@ -8,7 +8,6 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@@ -411,7 +410,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
private Path getInboxFolder(Long chatId) throws IOException {
Path baseInbox =
Paths.get(
Path.of(
runtimePathConfig.getPipelineWatchedFoldersPath(),
telegramProperties.getPipelineInboxFolder());
@@ -445,7 +444,7 @@ public class TelegramPipelineBot extends TelegramLongPollingBot {
private List<Path> waitForPipelineOutputs(PipelineFileInfo info) throws IOException {
Path finishedDir = Paths.get(runtimePathConfig.getPipelineFinishedFoldersPath());
Path finishedDir = Path.of(runtimePathConfig.getPipelineFinishedFoldersPath());
Files.createDirectories(finishedDir);
Instant start = info.savedAt();
@@ -3,7 +3,7 @@ package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.nio.file.Paths;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -34,9 +34,9 @@ class PrintFileControllerTest {
@Test
void printFile_absolutePath_throwsException() {
PrintFileRequest request = new PrintFileRequest();
String absPath = Paths.get("/etc/passwd").toString();
String absPath = Path.of("/etc/passwd").toString();
// Only test on systems where /etc/passwd is absolute
if (Paths.get(absPath).isAbsolute()) {
if (Path.of(absPath).isAbsolute()) {
MockMultipartFile file =
new MockMultipartFile(
"fileInput", absPath, "application/pdf", "data".getBytes());
@@ -207,16 +207,14 @@ public class AuditDashboardController {
// Include standard enum types in case they're not in the database yet
List<String> enumTypes =
Arrays.stream(AuditEventType.values())
.map(AuditEventType::name)
.collect(Collectors.toList());
Arrays.stream(AuditEventType.values()).map(AuditEventType::name).toList();
// Combine both sources, remove duplicates, and sort
Set<String> combinedTypes = new HashSet<>();
combinedTypes.addAll(dbTypes);
combinedTypes.addAll(enumTypes);
return combinedTypes.stream().sorted().collect(Collectors.toList());
return combinedTypes.stream().sorted().toList();
}
/** Export audit data as CSV. */
@@ -118,7 +118,7 @@ public class AuditRestController {
// Convert to response format expected by frontend
List<AuditEventDto> eventDtos =
events.getContent().stream().map(this::convertToDto).collect(Collectors.toList());
events.getContent().stream().map(this::convertToDto).toList();
AuditEventsResponse response =
AuditEventsResponse.builder()
@@ -191,19 +191,13 @@ public class AuditRestController {
ChartData eventsByTypeChart =
ChartData.builder()
.labels(new ArrayList<>(eventsByType.keySet()))
.values(
eventsByType.values().stream()
.map(Long::intValue)
.collect(Collectors.toList()))
.values(eventsByType.values().stream().map(Long::intValue).toList())
.build();
ChartData eventsByUserChart =
ChartData.builder()
.labels(new ArrayList<>(eventsByUser.keySet()))
.values(
eventsByUser.values().stream()
.map(Long::intValue)
.collect(Collectors.toList()))
.values(eventsByUser.values().stream().map(Long::intValue).toList())
.build();
// Sort events by day for time series
@@ -211,10 +205,7 @@ public class AuditRestController {
ChartData eventsOverTimeChart =
ChartData.builder()
.labels(new ArrayList<>(sortedEventsByDay.keySet()))
.values(
sortedEventsByDay.values().stream()
.map(Long::intValue)
.collect(Collectors.toList()))
.values(sortedEventsByDay.values().stream().map(Long::intValue).toList())
.build();
AuditChartsData chartsData =
@@ -239,16 +230,14 @@ public class AuditRestController {
// Include standard enum types in case they're not in the database yet
List<String> enumTypes =
Arrays.stream(AuditEventType.values())
.map(AuditEventType::name)
.collect(Collectors.toList());
Arrays.stream(AuditEventType.values()).map(AuditEventType::name).toList();
// Combine both sources, remove duplicates, and sort
Set<String> combinedTypes = new HashSet<>();
combinedTypes.addAll(dbTypes);
combinedTypes.addAll(enumTypes);
List<String> result = combinedTypes.stream().sorted().collect(Collectors.toList());
List<String> result = combinedTypes.stream().sorted().toList();
return ResponseEntity.ok(result);
}
@@ -263,11 +252,7 @@ public class AuditRestController {
// Use the countByPrincipal query to get unique principals
List<Object[]> principalCounts = auditRepository.countByPrincipal();
List<String> users =
principalCounts.stream()
.map(arr -> (String) arr[0])
.sorted()
.collect(Collectors.toList());
List<String> users = principalCounts.stream().map(arr -> (String) arr[0]).sorted().toList();
return ResponseEntity.ok(users);
}
@@ -3,7 +3,6 @@ package stirling.software.proprietary.controller.api;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
@@ -183,7 +182,7 @@ public class SignatureController {
*/
private boolean deleteFromSharedFolder(String signatureId) throws IOException {
String signatureBasePath = InstallationPathConfig.getSignaturesPath();
Path sharedFolder = Paths.get(signatureBasePath, ALL_USERS_FOLDER);
Path sharedFolder = Path.of(signatureBasePath, ALL_USERS_FOLDER);
boolean deleted = false;
if (Files.exists(sharedFolder)) {
@@ -3,7 +3,6 @@ package stirling.software.proprietary.controller.api;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -85,7 +84,7 @@ public class UsageRestController {
.build();
})
.sorted(Comparator.comparingInt(EndpointStatistic::getVisits).reversed())
.collect(Collectors.toList());
.toList();
// Apply limit if specified
if (limit != null && limit > 0 && statistics.size() > limit) {
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.configuration.ee;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Lazy;
@@ -104,7 +103,7 @@ public class LicenseKeyChecker {
if (keyOrFilePath.startsWith(FILE_PREFIX)) {
String filePath = keyOrFilePath.substring(FILE_PREFIX.length());
try {
Path path = Paths.get(filePath);
Path path = Path.of(filePath);
if (!Files.exists(path)) {
log.error("License file does not exist: {}", filePath);
return null;
@@ -4,7 +4,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
@@ -329,7 +328,7 @@ public class AdminLicenseController {
}
// Get config directory and target path
Path configPath = Paths.get(InstallationPathConfig.getConfigPath());
Path configPath = Path.of(InstallationPathConfig.getConfigPath());
Path configPathAbs = configPath.toAbsolutePath().normalize();
Path targetPath = configPathAbs.resolve(filename).normalize();
log.info(
@@ -702,7 +702,7 @@ public class AdminSettingsController {
if (path != null && !path.trim().isEmpty()) {
try {
java.nio.file.Path normalized =
java.nio.file.Paths.get(path.trim()).toAbsolutePath().normalize();
java.nio.file.Path.of(path.trim()).toAbsolutePath().normalize();
String normalizedStr = normalized.toString();
// Check for duplicates
@@ -719,9 +719,9 @@ public class AdminSettingsController {
// Check for overlapping paths
java.util.List<String> pathList = new java.util.ArrayList<>(normalizedPaths);
for (int i = 0; i < pathList.size(); i++) {
java.nio.file.Path path1 = java.nio.file.Paths.get(pathList.get(i));
java.nio.file.Path path1 = java.nio.file.Path.of(pathList.get(i));
for (int j = i + 1; j < pathList.size(); j++) {
java.nio.file.Path path2 = java.nio.file.Paths.get(pathList.get(j));
java.nio.file.Path path2 = java.nio.file.Path.of(pathList.get(j));
if (path1.startsWith(path2) || path2.startsWith(path1)) {
return "Overlapping paths detected: " + path1 + " and " + path2;
}
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.controller.api;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -280,7 +279,7 @@ public class InviteLinkController {
"expiresAt", invite.getExpiresAt().toString());
return inviteMap;
})
.collect(Collectors.toList());
.toList();
return ResponseEntity.ok(Map.of("invites", inviteList));
@@ -331,7 +330,7 @@ public class InviteLinkController {
List<InviteToken> expiredInvites =
inviteTokenRepository.findAll().stream()
.filter(invite -> !invite.isValid())
.collect(Collectors.toList());
.toList();
int count = expiredInvites.size();
inviteTokenRepository.deleteAll(expiredInvites);
@@ -6,7 +6,6 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.regex.Pattern;
@@ -53,7 +52,7 @@ public class UIDataTessdataController {
TessdataLanguagesResponse response = new TessdataLanguagesResponse();
response.setInstalled(getAvailableTesseractLanguages());
response.setAvailable(getRemoteTessdataLanguages());
response.setWritable(isWritableDirectory(Paths.get(runtimePathConfig.getTessDataPath())));
response.setWritable(isWritableDirectory(Path.of(runtimePathConfig.getTessDataPath())));
return ResponseEntity.ok(response);
}
@@ -67,7 +66,7 @@ public class UIDataTessdataController {
.body(Map.of("message", "No languages provided for download"));
}
Path tessdataDir = Paths.get(runtimePathConfig.getTessDataPath());
Path tessdataDir = Path.of(runtimePathConfig.getTessDataPath());
try {
Files.createDirectories(tessdataDir);
} catch (IOException e) {
@@ -1009,7 +1009,7 @@ public class UserController {
source.stream()
.filter(User::isEnabled)
.map(this::toUserSummaryDTO)
.collect(java.util.stream.Collectors.toList());
.toList();
return ResponseEntity.ok(users);
}
@@ -4,7 +4,6 @@ import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
@@ -23,7 +22,6 @@ import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.sql.DataSource;
@@ -100,7 +98,7 @@ public class DatabaseService implements DatabaseServiceInterface {
ApplicationProperties.Datasource datasourceProps,
DataSource dataSource,
DatabaseNotificationServiceInterface backupNotificationService) {
this.BACKUP_DIR = Paths.get(InstallationPathConfig.getBackupPath()).normalize();
this.BACKUP_DIR = Path.of(InstallationPathConfig.getBackupPath()).normalize();
this.datasourceProps = datasourceProps;
this.dataSource = dataSource;
this.backupNotificationService = backupNotificationService;
@@ -111,7 +109,7 @@ public class DatabaseService implements DatabaseServiceInterface {
@Deprecated(since = "2.0.0", forRemoval = true)
private void moveBackupFiles() {
Path sourceDir =
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
Path.of(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
if (!Files.exists(sourceDir)) {
log.info("Source directory does not exist: {}", sourceDir);
@@ -217,7 +215,7 @@ public class DatabaseService implements DatabaseServiceInterface {
List<FileInfo> backupList = this.getBackupList();
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
Path latestExport = Paths.get(backupList.get(0).getFilePath());
Path latestExport = Path.of(backupList.get(0).getFilePath());
executeDatabaseScript(latestExport);
}
@@ -258,9 +256,13 @@ public class DatabaseService implements DatabaseServiceInterface {
@Override
public void exportDatabase() {
List<FileInfo> filteredBackupList =
this.getBackupList().stream()
.filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_"))
.collect(Collectors.toList());
new ArrayList<>(
this.getBackupList().stream()
.filter(
backup ->
!backup.getFileName()
.startsWith(BACKUP_PREFIX + "user_"))
.toList());
if (filteredBackupList.size() > 5) {
deleteOldestBackup(filteredBackupList);
@@ -341,7 +343,7 @@ public class DatabaseService implements DatabaseServiceInterface {
for (FileInfo backup : backupList) {
try {
Files.deleteIfExists(Paths.get(backup.getFilePath()));
Files.deleteIfExists(Path.of(backup.getFilePath()));
deletedFiles.add(Pair.of(backup, true));
} catch (IOException e) {
log.error("Error deleting backup file: {}", backup.getFileName(), e);
@@ -359,7 +361,7 @@ public class DatabaseService implements DatabaseServiceInterface {
if (!backupList.isEmpty()) {
FileInfo lastBackup = backupList.get(backupList.size() - 1);
try {
Files.deleteIfExists(Paths.get(lastBackup.getFilePath()));
Files.deleteIfExists(Path.of(lastBackup.getFilePath()));
deletedFiles.add(Pair.of(lastBackup, true));
} catch (IOException e) {
log.error("Error deleting last backup file: {}", lastBackup.getFileName(), e);
@@ -381,7 +383,7 @@ public class DatabaseService implements DatabaseServiceInterface {
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
FileInfo oldestFile = filteredBackupList.get(0);
Files.deleteIfExists(Paths.get(oldestFile.getFilePath()));
Files.deleteIfExists(Path.of(oldestFile.getFilePath()));
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
} catch (IOException e) {
log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e);
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -76,7 +75,7 @@ public class KeyPairCleanupService {
return;
}
Path privateKeyDirectory = Paths.get(InstallationPathConfig.getPrivateKeyPath());
Path privateKeyDirectory = Path.of(InstallationPathConfig.getPrivateKeyPath());
Path keyFile = privateKeyDirectory.resolve(keyId + KeyPersistenceService.KEY_SUFFIX);
if (Files.exists(keyFile)) {
@@ -3,7 +3,6 @@ package stirling.software.proprietary.security.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -20,7 +19,6 @@ import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
@@ -82,7 +80,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
*/
private void loadExistingKeysFromDisk() {
try {
Path keyDirectory = Paths.get(InstallationPathConfig.getPrivateKeyPath());
Path keyDirectory = Path.of(InstallationPathConfig.getPrivateKeyPath());
if (!Files.exists(keyDirectory)) {
log.info("No existing keys found, generating new keypair");
@@ -99,7 +97,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
b.getFileName().compareTo(a.getFileName())) // Most
// recent
// first
.collect(Collectors.toList());
.toList();
}
if (keyFiles.isEmpty()) {
@@ -275,7 +273,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
eligible);
return eligible;
})
.collect(Collectors.toList());
.toList();
}
private String generateKeyId() {
@@ -284,20 +282,17 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
}
private KeyPair generateRSAKeypair() {
KeyPairGenerator keyPairGenerator = null;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
log.error("Failed to initialize RSA key pair generator", e);
throw new IllegalStateException("RSA key pair generator is not available", e);
}
return keyPairGenerator.generateKeyPair();
}
private void ensurePrivateKeyDirectoryExists() throws IOException {
Path keyPath = Paths.get(InstallationPathConfig.getPrivateKeyPath());
Path keyPath = Path.of(InstallationPathConfig.getPrivateKeyPath());
if (!Files.exists(keyPath)) {
Files.createDirectories(keyPath);
@@ -312,7 +307,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
* <p>Public key stored as: keyId.pub
*/
private void storeKeyPair(String keyId, KeyPair keyPair) throws IOException {
Path keyDirectory = Paths.get(InstallationPathConfig.getPrivateKeyPath());
Path keyDirectory = Path.of(InstallationPathConfig.getPrivateKeyPath());
// Store private key
Path privateKeyFile = keyDirectory.resolve(keyId + KEY_SUFFIX);
@@ -346,7 +341,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
private PrivateKey loadPrivateKey(String keyId)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Path keyFile =
Paths.get(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + KEY_SUFFIX);
Path.of(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + KEY_SUFFIX);
if (!Files.exists(keyFile)) {
throw new IOException("Private key not found: " + keyFile);
@@ -369,8 +364,7 @@ public class KeyPersistenceService implements KeyPersistenceServiceInterface {
*/
private String loadPublicKey(String keyId) throws IOException {
Path publicKeyFile =
Paths.get(InstallationPathConfig.getPrivateKeyPath())
.resolve(keyId + PUB_KEY_SUFFIX);
Path.of(InstallationPathConfig.getPrivateKeyPath()).resolve(keyId + PUB_KEY_SUFFIX);
if (!Files.exists(publicKeyFile)) {
throw new IOException("Public key not found: " + publicKeyFile);
@@ -5,7 +5,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@@ -101,7 +100,7 @@ public class LoginAttemptService {
return attemptsCache.entrySet().stream()
.filter(entry -> entry.getValue().getAttemptCount() >= MAX_ATTEMPT)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
.toList();
}
public int getRemainingAttempts(String key) {
@@ -31,21 +31,14 @@ public class RefreshRateLimitService {
this.jwtProperties = applicationProperties.getSecurity().getJwt();
}
private static class RefreshAttempt {
private final AtomicInteger count = new AtomicInteger(0);
private final Instant firstAttempt = Instant.now();
private record RefreshAttempt(AtomicInteger count, Instant firstAttempt) {
RefreshAttempt() {
this(new AtomicInteger(0), Instant.now());
}
int incrementAndGet() {
return count.incrementAndGet();
}
Instant getFirstAttempt() {
return firstAttempt;
}
int getCount() {
return count.get();
}
}
private final Map<String, RefreshAttempt> attempts = new ConcurrentHashMap<>();
@@ -72,7 +65,7 @@ public class RefreshRateLimitService {
// Clean up if outside grace window
Instant cutoff = Instant.now().minusMillis(graceWindowMillis);
if (attempt.getFirstAttempt().isBefore(cutoff)) {
if (attempt.firstAttempt().isBefore(cutoff)) {
attempts.remove(tokenHash);
}
@@ -98,15 +91,9 @@ public class RefreshRateLimitService {
? configuredMinutes
: JwtConstants.DEFAULT_REFRESH_GRACE_MINUTES;
Instant cutoff = Instant.now().minusMillis(graceMinutes * 60000L);
int removed =
attempts.entrySet().stream()
.filter(entry -> entry.getValue().getFirstAttempt().isBefore(cutoff))
.mapToInt(
entry -> {
attempts.remove(entry.getKey());
return 1;
})
.sum();
int beforeSize = attempts.size();
attempts.entrySet().removeIf(entry -> entry.getValue().firstAttempt().isBefore(cutoff));
int removed = beforeSize - attempts.size();
if (removed > 0) {
log.debug("Cleaned up {} expired refresh tracking entries", removed);
@@ -11,7 +11,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.StringUtils;
@@ -413,7 +412,7 @@ public class AuditService {
return m;
})
.collect(Collectors.toList());
.toList();
data.put("files", fileInfos);
}
@@ -4,7 +4,6 @@ import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
@@ -64,7 +63,7 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
}
private Path getKeystorePath() {
return Paths.get(InstallationPathConfig.getConfigPath(), KEYSTORE_FILENAME);
return Path.of(InstallationPathConfig.getConfigPath(), KEYSTORE_FILENAME);
}
private boolean hasProOrEnterpriseAccess() {
@@ -5,7 +5,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Base64;
@@ -55,7 +54,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
@Override
public byte[] getPersonalSignatureBytes(String username, String fileName) throws IOException {
validateFileName(fileName);
Path userPath = Paths.get(SIGNATURE_BASE_PATH, username, fileName);
Path userPath = Path.of(SIGNATURE_BASE_PATH, username, fileName);
if (!Files.exists(userPath)) {
throw new FileNotFoundException("Personal signature not found");
@@ -76,7 +75,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
}
String folderName = "shared".equals(scope) ? ALL_USERS_FOLDER : username;
Path targetFolder = Paths.get(SIGNATURE_BASE_PATH, folderName);
Path targetFolder = Path.of(SIGNATURE_BASE_PATH, folderName);
// Only enforce limits for personal signatures (not shared)
if ("personal".equals(scope)) {
@@ -170,13 +169,13 @@ public class SignatureService implements PersonalSignatureServiceInterface {
List<SavedSignatureResponse> signatures = new ArrayList<>();
// Load personal signatures
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
if (Files.exists(personalFolder)) {
signatures.addAll(loadSignaturesFromFolder(personalFolder, "personal", true));
}
// Load shared signatures
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
if (Files.exists(sharedFolder)) {
signatures.addAll(loadSignaturesFromFolder(sharedFolder, "shared", false));
}
@@ -189,7 +188,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
validateFileName(signatureId);
// Only allow deletion from personal folder
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
boolean deleted = false;
if (Files.exists(personalFolder)) {
@@ -227,7 +226,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
validateFileName(signatureId);
// Try personal folder first
Path personalFolder = Paths.get(SIGNATURE_BASE_PATH, username);
Path personalFolder = Path.of(SIGNATURE_BASE_PATH, username);
Path metadataPath = personalFolder.resolve(signatureId + ".json");
if (Files.exists(metadataPath)) {
@@ -237,7 +236,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
}
// If not found in personal, try shared folder
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
Path sharedMetadataPath = sharedFolder.resolve(signatureId + ".json");
if (Files.exists(sharedMetadataPath)) {
@@ -251,7 +250,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
public boolean isSharedSignature(String signatureId) {
validateFileName(signatureId);
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
Path sharedFolder = Path.of(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
return Files.exists(sharedFolder.resolve(signatureId + ".json"));
}
@@ -274,7 +273,7 @@ public class SignatureService implements PersonalSignatureServiceInterface {
// Private helper methods
private void enforceStorageLimits(String username, String dataUrlToAdd) throws IOException {
Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username);
Path userFolder = Path.of(SIGNATURE_BASE_PATH, username);
if (!Files.exists(userFolder)) {
return; // First signature, no limits to check
@@ -3,7 +3,6 @@ package stirling.software.proprietary.storage.config;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Optional;
@@ -58,8 +57,8 @@ public class StorageProviderConfig {
}
basePathValue = InstallationPathConfig.getPath() + "storage";
}
Path basePath = Paths.get(basePathValue).toAbsolutePath().normalize();
Path installRoot = Paths.get(InstallationPathConfig.getPath()).toAbsolutePath().normalize();
Path basePath = Path.of(basePathValue).toAbsolutePath().normalize();
Path installRoot = Path.of(InstallationPathConfig.getPath()).toAbsolutePath().normalize();
if (!basePath.startsWith(installRoot)) {
// Warn rather than hard-fail: admins may legitimately point storage at an external
// volume, but an unexpected path could indicate a misconfiguration or traversal
@@ -4,7 +4,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
import java.util.UUID;
@@ -80,7 +79,7 @@ public class LocalStorageProvider implements StorageProvider {
if (filename == null || filename.isBlank()) {
return "file";
}
String stripped = Paths.get(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
String stripped = Path.of(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
return stripped.isBlank() ? "file" : stripped;
}
}
@@ -4,7 +4,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
@@ -150,7 +150,7 @@ public class S3StorageProvider implements StorageProvider, AutoCloseable {
if (originalFilename == null || originalFilename.isBlank()) {
return null;
}
// Strip CR/LF and other control chars before path parsing (Paths.get throws on them on
// Strip CR/LF and other control chars before path parsing (Path.of throws on them on
// Windows, and they defeat header parsers).
String stripped = originalFilename.replaceAll("\\p{Cntrl}", "");
// Use only the basename to avoid leaking directory structure into the header.
@@ -184,7 +184,7 @@ public class S3StorageProvider implements StorageProvider, AutoCloseable {
if (filename == null || filename.isBlank()) {
return "file";
}
String stripped = Paths.get(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
String stripped = Path.of(filename).getFileName().toString().replaceAll("\\p{Cntrl}", "");
return stripped.isBlank() ? "file" : stripped;
}
}
@@ -14,7 +14,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
@@ -361,7 +360,7 @@ public class FileStorageService {
return files.stream()
.sorted(Comparator.comparing(StoredFile::getCreatedAt).reversed())
.map(file -> buildResponse(file, user, roleByFileId.get(file.getId())))
.collect(Collectors.toList());
.toList();
}
public StoredFileResponse getAccessibleFileResponse(User user, Long fileId) {
@@ -400,7 +399,7 @@ public class FileStorageService {
.filter(Objects::nonNull)
.map(User::getUsername)
.sorted(String.CASE_INSENSITIVE_ORDER)
.collect(Collectors.toList())
.toList()
: List.of();
List<ShareLinkResponse> shareLinks =
ownedByCurrentUser && isShareLinksEnabled()
@@ -419,7 +418,7 @@ public class FileStorageService {
.expiresAt(share.getExpiresAt())
.build())
.sorted(Comparator.comparing(ShareLinkResponse::getCreatedAt))
.collect(Collectors.toList())
.toList()
: List.of();
List<SharedUserResponse> sharedUsers =
ownedByCurrentUser
@@ -440,7 +439,7 @@ public class FileStorageService {
Comparator.comparing(
SharedUserResponse::getUsername,
String.CASE_INSENSITIVE_ORDER))
.collect(Collectors.toList())
.toList()
: List.of();
return StoredFileResponse.builder()
.id(file.getId())
@@ -762,7 +761,7 @@ public class FileStorageService {
.accessType(access.getAccessType().name())
.accessedAt(access.getAccessedAt())
.build())
.collect(Collectors.toList());
.toList();
}
public List<FileShareAccess> listAccessedShareLinks(User user) {
@@ -818,7 +817,7 @@ public class FileStorageService {
.build();
})
.filter(response -> response.getShareToken() != null)
.collect(Collectors.toList());
.toList();
}
public void ensureSharingEnabled() {
@@ -1007,7 +1006,7 @@ public class FileStorageService {
file.getHistoryStorageKey(),
file.getAuditLogStorageKey())
.filter(value -> value != null && !value.isBlank())
.collect(Collectors.toList());
.toList();
}
private void cleanupStoredObject(StoredObject storedObject) {
@@ -76,7 +76,7 @@ public class SigningSessionController {
.map(
stirling.software.proprietary.workflow.util.WorkflowMapper
::toResponse)
.collect(java.util.stream.Collectors.toList());
.toList();
return ResponseEntity.ok(responses);
} catch (Exception e) {
log.error("Error listing sessions for user {}", principal.getName(), e);
@@ -98,7 +98,7 @@ public class SigningFinalizationService {
// Step 1.5: Add summary page BEFORE digital signing (if enabled)
// CRITICAL: Must be done before signing to avoid invalidating signatures
if (Boolean.TRUE.equals(settings.includeSummaryPage)) {
if (Boolean.TRUE.equals(settings.includeSummaryPage())) {
log.info(
"Adding summary page before digital signing for session {}",
session.getSessionId());
@@ -108,11 +108,13 @@ public class SigningFinalizationService {
// Suppress digital certificate visual block when summary page is enabled
// (wet signatures already applied in Step 1 and will still appear)
Boolean showVisualSignature =
Boolean.TRUE.equals(settings.includeSummaryPage) ? false : settings.showSignature;
Boolean.TRUE.equals(settings.includeSummaryPage())
? false
: settings.showSignature();
log.info(
"Finalization settings: includeSummaryPage={}, showVisualSignature={}",
settings.includeSummaryPage,
settings.includeSummaryPage(),
showVisualSignature);
// Step 2: Apply digital certificates per SIGNED participant
@@ -150,8 +152,8 @@ public class SigningFinalizationService {
log.info(
"Applying signature for {} with reason='{}', location='{}'",
fresh.getEmail(),
sigMeta.reason,
sigMeta.location);
sigMeta.reason(),
sigMeta.location());
pdf =
applyDigitalSignature(
@@ -159,10 +161,10 @@ public class SigningFinalizationService {
fresh,
submission,
showVisualSignature,
settings.pageNumber,
sigMeta.reason,
sigMeta.location,
settings.showLogo);
settings.pageNumber(),
sigMeta.reason(),
sigMeta.location(),
settings.showLogo());
}
return pdf;
@@ -682,7 +684,7 @@ public class SigningFinalizationService {
textDark,
textMuted,
"Subject:",
certInfo.subjectCN);
certInfo.subjectCN());
rRowY -= LINE_H;
drawLabelValue(
cs,
@@ -693,7 +695,7 @@ public class SigningFinalizationService {
textDark,
textMuted,
"Issuer:",
certInfo.issuerCN);
certInfo.issuerCN());
rRowY -= LINE_H;
drawLabelValue(
cs,
@@ -704,7 +706,7 @@ public class SigningFinalizationService {
textDark,
textMuted,
"Serial:",
certInfo.serialNumber);
certInfo.serialNumber());
rRowY -= LINE_H;
drawLabelValue(
cs,
@@ -715,7 +717,7 @@ public class SigningFinalizationService {
textDark,
textMuted,
"Valid From:",
certInfo.validFrom);
certInfo.validFrom());
rRowY -= LINE_H;
drawLabelValue(
cs,
@@ -726,7 +728,7 @@ public class SigningFinalizationService {
textDark,
textMuted,
"Valid Until:",
certInfo.validUntil);
certInfo.validUntil());
rRowY -= LINE_H;
drawLabelValue(
cs,
@@ -737,7 +739,7 @@ public class SigningFinalizationService {
textDark,
textMuted,
"Algorithm:",
certInfo.algorithm);
certInfo.algorithm());
}
yPos -= cardH + 12;
@@ -1075,10 +1077,9 @@ public class SigningFinalizationService {
}
String alias = aliases.nextElement();
Certificate cert = keystore.getCertificate(alias);
if (!(cert instanceof X509Certificate)) {
if (!(cert instanceof X509Certificate x509)) {
return null;
}
X509Certificate x509 = (X509Certificate) cert;
String subjectCN = extractCN(x509.getSubjectX500Principal().getName());
String issuerCN = extractCN(x509.getIssuerX500Principal().getName());
@@ -1256,55 +1257,19 @@ public class SigningFinalizationService {
// ===== PRIVATE INNER TYPES =====
private static class SessionSignatureSettings {
final Boolean showSignature;
final Integer pageNumber;
final Boolean showLogo;
final Boolean includeSummaryPage;
private record SessionSignatureSettings(
Boolean showSignature,
Integer pageNumber,
Boolean showLogo,
Boolean includeSummaryPage) {}
SessionSignatureSettings(
Boolean showSignature,
Integer pageNumber,
Boolean showLogo,
Boolean includeSummaryPage) {
this.showSignature = showSignature;
this.pageNumber = pageNumber;
this.showLogo = showLogo;
this.includeSummaryPage = includeSummaryPage;
}
}
private record ParticipantSignatureMetadata(String reason, String location) {}
private static class ParticipantSignatureMetadata {
final String reason;
final String location;
ParticipantSignatureMetadata(String reason, String location) {
this.reason = reason;
this.location = location;
}
}
private static class CertificateInfo {
final String subjectCN;
final String issuerCN;
final String serialNumber;
final String validFrom;
final String validUntil;
final String algorithm;
CertificateInfo(
String subjectCN,
String issuerCN,
String serialNumber,
String validFrom,
String validUntil,
String algorithm) {
this.subjectCN = subjectCN;
this.issuerCN = issuerCN;
this.serialNumber = serialNumber;
this.validFrom = validFrom;
this.validUntil = validUntil;
this.algorithm = algorithm;
}
}
private record CertificateInfo(
String subjectCN,
String issuerCN,
String serialNumber,
String validFrom,
String validUntil,
String algorithm) {}
}
@@ -553,7 +553,7 @@ public class WorkflowSessionService {
dto.setMyStatus(p.getStatus());
return dto;
})
.collect(java.util.stream.Collectors.toList());
.toList();
}
/**
@@ -3,7 +3,6 @@ package stirling.software.proprietary.workflow.util;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -80,12 +79,12 @@ public class WorkflowMapper {
response.setParticipants(
session.getParticipants().stream()
.map(p -> toParticipantResponse(p, objectMapper, includeShareTokens))
.collect(Collectors.toList()));
.toList());
} else {
response.setParticipants(
session.getParticipants().stream()
.map(p -> toParticipantResponse(p, includeShareTokens))
.collect(Collectors.toList()));
.toList());
}
// Calculate participant counts
+1 -1
View File
@@ -5,7 +5,7 @@ ARG BASE_VERSION=1.0.2
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
# Stage 1: Build the Java application and frontend
FROM gradle:9.3.1-jdk25@sha256:85aec999629f4774a383cb792da4b598bdf5a7e69c4b9570bb70c0f919179183 AS app-build
FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS app-build
ARG TASK_VERSION=3.49.1
RUN apt-get update \
+1 -1
View File
@@ -6,7 +6,7 @@ ARG BASE_VERSION=1.0.2
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
# Stage 1: Build the Java application and frontend
FROM gradle:9.3.1-jdk25 AS app-build
FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS app-build
ARG TASK_VERSION=3.49.1
RUN apt-get update \
+1 -1
View File
@@ -2,7 +2,7 @@
# Single JAR contains both frontend and backend with minimal dependencies
# Stage 1: Build application with embedded frontend
FROM gradle:9.3.1-jdk25 AS build
FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS build
# Install Node.js and npm for frontend build
ARG TASK_VERSION=3.49.1
@@ -5,6 +5,7 @@ import json
from datetime import datetime
import psycopg
from pgvector import Vector
from pgvector.psycopg import register_vector_async
from psycopg_pool import AsyncConnectionPool
@@ -68,8 +69,11 @@ class PgVectorStore(DocumentStore):
self._initialized = True
async def _bootstrap_schema(self) -> None:
# Uses a dedicated non-pool connection: pooled connections register the
# vector type on checkout, which fails until the extension exists.
# The `vector` type must exist before the pool's configure hook
# (register_vector_async) can resolve it, and on a fresh database it doesn't
# yet. Create the extension + schema on a raw, non-pool connection that hasn't
# registered the type; _ensure_ready then opens the pool, whose configure hook
# registers the now-existing type per connection.
async with await psycopg.AsyncConnection.connect(self._dsn) as conn:
async with conn.cursor() as cur:
await cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
@@ -230,7 +234,7 @@ class PgVectorStore(DocumentStore):
metadata = EXCLUDED.metadata,
embedding = EXCLUDED.embedding
""",
(doc.id, collection, owner_id, doc.text, json.dumps(doc.metadata), emb),
(doc.id, collection, owner_id, doc.text, json.dumps(doc.metadata), Vector(emb)),
)
await conn.commit()
@@ -307,6 +311,7 @@ class PgVectorStore(DocumentStore):
owner_id = await self._readable_owner_for(cur, collection, principals)
if owner_id is None:
return []
query_vec = Vector(query_embedding)
await cur.execute(
"""
SELECT id, text, metadata, 1 - (embedding <=> %s) AS score
@@ -315,7 +320,7 @@ class PgVectorStore(DocumentStore):
ORDER BY embedding <=> %s
LIMIT %s
""",
(query_embedding, collection, owner_id, query_embedding, top_k),
(query_vec, collection, owner_id, query_vec, top_k),
)
rows = await cur.fetchall()
@@ -48,11 +48,18 @@ X64_JRE="$WORK_DIR/jre-x86_64"
run_jlink() {
local java_home="$1"
local out="$2"
local compress
if "$java_home/bin/jlink" --help 2>&1 | grep -q 'zip-\[0-9\]'; then
compress="zip-6"
else
compress="2"
fi
"$java_home/bin/jlink" \
--module-path "$java_home/jmods" \
--add-modules "$JLINK_MODULES" \
--strip-debug \
--compress=zip-6 \
--compress="$compress" \
--no-header-files \
--no-man-pages \
--output "$out"
+20 -6
View File
@@ -1,13 +1,14 @@
import { defineConfig, loadEnv, type PluginOption } from "vite";
import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from "vite-tsconfig-paths";
import { viteStaticCopy } from "vite-plugin-static-copy";
import { compression, defineAlgorithm } from "vite-plugin-compression2";
import { constants, gzip, brotliCompress } from "node:zlib";
import fs from "node:fs/promises";
import path, { resolve } from "node:path";
import { constants, brotliCompress, gzip } from "node:zlib";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import fs from "node:fs/promises";
import path from "node:path";
import { defineConfig, loadEnv } from "vite";
import type { PluginOption } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { viteStaticCopy } from "vite-plugin-static-copy";
const gzipPromise = promisify(gzip);
const brotliPromise = promisify(brotliCompress);
@@ -96,6 +97,7 @@ export default defineConfig(async ({ mode }) => {
// this file lived at frontend/, but after the editor was moved under
// frontend/editor/ the cwd-based lookup would miss editor/.env*.
const env = loadEnv(mode, import.meta.dirname, "");
const parentEnv = loadEnv(mode, resolve(import.meta.dirname, ".."), "");
// Effective mode: --mode > STIRLING_FLAVOR > ENABLE_SAAS > DISABLE_ADDITIONAL_FEATURES > proprietary.
const explicitMode = (VALID_MODES as readonly string[]).includes(mode)
@@ -120,6 +122,17 @@ export default defineConfig(async ({ mode }) => {
// Backend proxy target: default localhost:8080. Override via BACKEND_URL env var
// so the top-level dev launcher can wire a dynamically-assigned backend port.
const backendUrl = process.env.BACKEND_URL || "http://localhost:8080";
// Allow host header checks to be configured via env so LAN/reverse-proxy
// dev setups don't require editing this file for each machine.
const allowedHostsRaw =
process.env.FRONTEND_ALLOWED_HOSTS ||
env.FRONTEND_ALLOWED_HOSTS ||
parentEnv.FRONTEND_ALLOWED_HOSTS ||
"";
const allowedHosts = allowedHostsRaw
.split(",")
.map((host) => host.trim())
.filter(Boolean);
const backendProxy = {
target: backendUrl,
changeOrigin: true,
@@ -214,6 +227,7 @@ export default defineConfig(async ({ mode }) => {
},
server: {
host: true,
allowedHosts: allowedHosts.length > 0 ? allowedHosts : undefined,
// make sure this port matches the devUrl port in tauri.conf.json file
port: 5173,
// Tauri expects a fixed port, fail if that port is not available
Binary file not shown.
+3 -1
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+1 -1
View File
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
Vendored
+10 -21
View File
@@ -23,8 +23,8 @@
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,7 +65,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@@ -73,21 +73,10 @@ goto fail
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+1 -1
View File
@@ -12,7 +12,7 @@ public class PropSync {
File folder = new File("C:\\Users\\systo\\git\\Stirling-PDF\\app\\core\\src\\main\\resources");
File[] files = folder.listFiles((dir, name) -> name.matches("messages_.*\\.properties"));
List<String> enLines = Files.readAllLines(Paths.get(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8);
List<String> enLines = Files.readAllLines(Path.of(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8);
Map<String, String> enProps = linesToProps(enLines);
for (File file : files) {
+2 -2
View File
@@ -19,9 +19,9 @@ public class RestartHelper {
Map<String, String> cli = parseArgs(args);
long pid = Long.parseLong(req(cli, "pid"));
Path appJar = Paths.get(req(cli, "app")).toAbsolutePath().normalize();
Path appJar = Path.of(req(cli, "app")).toAbsolutePath().normalize();
String javaBin = cli.getOrDefault("java", "java");
Path argsFile = cli.containsKey("argsFile") ? Paths.get(cli.get("argsFile")) : null;
Path argsFile = cli.containsKey("argsFile") ? Path.of(cli.get("argsFile")) : null;
long backoffMs = Long.parseLong(cli.getOrDefault("backoffMs", "1000"));
if (!Files.isRegularFile(appJar)) {
+57 -2
View File
@@ -10,6 +10,8 @@
# Inputs:
# STIRLING_JAR - path to a pre-built Stirling-PDF .jar (defaults to the
# :stirling-pdf:bootJar output)
# JAVA_BIN - override the Java executable used to launch the JAR
# (defaults to MIGRATION_TEST_JAVA, JAVA_HOME, then PATH)
# FIXTURE_DIR - override the fixture directory (rarely needed)
#
# Exits non-zero on any fixture failure and writes a summary to stderr.
@@ -19,6 +21,7 @@ set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
FIXTURE_DIR="${FIXTURE_DIR:-$REPO_ROOT/app/proprietary/src/test/resources/db-migration-fixtures}"
STIRLING_JAR="${STIRLING_JAR:-}"
JAVA_BIN="${JAVA_BIN:-${MIGRATION_TEST_JAVA:-}}"
ADMIN_USERNAME="${ADMIN_USERNAME:-admin}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-stirling}"
STARTUP_TIMEOUT_SEC="${STARTUP_TIMEOUT_SEC:-300}"
@@ -26,6 +29,52 @@ STARTUP_TIMEOUT_SEC="${STARTUP_TIMEOUT_SEC:-300}"
log() { printf '[migration-test] %s\n' "$*" >&2; }
fail() { printf '[migration-test][FAIL] %s\n' "$*" >&2; exit 1; }
java_major_version() {
local java_bin="$1"
"$java_bin" -XshowSettings:properties -version 2>&1 \
| awk -F'= ' '/java.specification.version =/ { print $2; exit }'
}
find_java() {
local candidate
if [[ -n "$JAVA_BIN" ]]; then
if [[ -x "$JAVA_BIN" ]]; then
candidate="$JAVA_BIN"
elif command -v "$JAVA_BIN" >/dev/null 2>&1; then
candidate=$(command -v "$JAVA_BIN")
else
fail "JAVA_BIN/MIGRATION_TEST_JAVA='$JAVA_BIN' is not executable or on PATH"
fi
elif [[ -n "${JAVA_HOME:-}" && -x "${JAVA_HOME}/bin/java" ]]; then
candidate="${JAVA_HOME}/bin/java"
else
local java_home_var
for java_home_var in JAVA_HOME_25_X64 JAVA_HOME_25_ARM64 JAVA_HOME_25_AARCH64; do
local java_home="${!java_home_var:-}"
if [[ -n "$java_home" && -x "$java_home/bin/java" ]]; then
candidate="$java_home/bin/java"
break
fi
done
if [[ -z "${candidate:-}" ]]; then
candidate=$(command -v java || true)
fi
fi
[[ -n "${candidate:-}" ]] || fail "No java executable found; install JDK 25 or set JAVA_BIN"
realpath "$candidate"
}
assert_supported_java() {
local java_bin="$1"
local major
major=$(java_major_version "$java_bin")
[[ -n "$major" ]] || fail "Could not determine Java version from '$java_bin'"
if (( major < 25 )); then
fail "Migration test requires JDK 25+ to run the built JAR, but '$java_bin' is Java $major. Set JAVA_HOME or JAVA_BIN to a JDK 25 installation."
fi
}
find_jar() {
local candidate
if [[ -n "$STIRLING_JAR" ]]; then
@@ -63,7 +112,9 @@ test_fixture() {
label=$(basename "$fixture_path" .mv.db)
log "=== $label ==="
local jar; jar=$(find_jar)
local jar
jar=$(find_jar)
local java_bin="$MIGRATION_JAVA_BIN"
local workdir; workdir=$(mktemp -d)
local configsdir="$workdir/configs"
mkdir -p "$configsdir"
@@ -74,6 +125,7 @@ test_fixture() {
local log_file="$workdir/app.log"
log " jar=$jar"
log " java=$java_bin ($("$java_bin" -version 2>&1 | head -n 1))"
log " workdir=$workdir"
log " port=$port"
@@ -86,7 +138,7 @@ test_fixture() {
# to cwd, and we want to make sure we hit the fixture's configs/ and not
# whatever happens to live at the runner's working directory.
pushd "$workdir" >/dev/null
java -Xmx1g -jar "$jar" \
"$java_bin" -Xmx1g -jar "$jar" \
"--server.port=$port" \
"--spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE;MODE=PostgreSQL" \
"--spring.jpa.show-sql=false" \
@@ -169,6 +221,9 @@ test_fixture() {
main() {
[[ -d "$FIXTURE_DIR" ]] || fail "Fixture dir not found: $FIXTURE_DIR"
MIGRATION_JAVA_BIN=$(find_java)
assert_supported_java "$MIGRATION_JAVA_BIN"
local fixtures
mapfile -t fixtures < <(find "$FIXTURE_DIR" -maxdepth 1 -name '*.mv.db' | sort)
[[ ${#fixtures[@]} -gt 0 ]] || fail "No fixtures under $FIXTURE_DIR"
+12 -3
View File
@@ -200,12 +200,12 @@ _ISE_STEMS = [
"characteri",
"stabili",
"generali",
"specifi? ".strip(),
"specifi? ".strip(), # codespell:ignore specifi
"sterili",
"neutrali",
"saniti",
"tokeni",
"serie? ".strip(),
"serie? ".strip(), # codespell:ignore serie
"alphabeti",
"synthesi",
"memori",
@@ -213,7 +213,16 @@ _ISE_STEMS = [
"colouri",
]
_ISE_STEMS = [s for s in _ISE_STEMS if s and not s.endswith("?")]
_ISE_SUFFIXES = ["se", "ses", "sed", "sing", "ser", "sers", "sation", "sations"]
_ISE_SUFFIXES = [
"se",
"ses",
"sed",
"sing",
"ser", # codespell:ignore ser
"sers",
"sation",
"sations",
]
for stem in _ISE_STEMS:
for suf in _ISE_SUFFIXES:
add(stem + suf, stem + suf.replace("s", "z", 1))
+40
View File
@@ -482,6 +482,46 @@ title = "Add Page Numbers"
Keys use dot notation internally (e.g., `addPageNumbers.selectText.1`).
### Pluralization Suffixes
Pluralized i18n keys use the i18next/Intl plural suffixes:
`_zero`, `_one`, `_two`, `_few`, `_many`, and `_other`.
Define the shared base key plus one entry for each plural category the language
needs. Always provide `_other`; i18next uses it as the fallback plural form.
Use `{{count}}` as the count placeholder, and call the base key from code with a
`count` value:
```toml
[common]
fileCount_one = "{{count}} file"
fileCount_other = "{{count}} files"
fileCount_zero = "No files"
```
Languages with more plural categories can add their required forms. For example,
Arabic-style plural rules distinguish `0`, `1`, `2`, small counts, large counts,
and the fallback form:
```toml
[common]
uploadCount_zero = "No files uploaded" # count = 0
uploadCount_one = "One file uploaded" # count = 1
uploadCount_two = "Two files uploaded" # count = 2
uploadCount_few = "{{count}} files uploaded" # count = 3-10
uploadCount_many = "{{count}} files uploaded" # count = 11-99
uploadCount_other = "{{count}} files uploaded" # count = 100, fractions, fallback
```
```ts
t("common.uploadCount", { count: files.length });
```
Do not translate the suffix names themselves. They are CLDR plural categories,
not English words. Add only the forms required by the target language, but keep
the plural key set aligned with `en-US` unless the target language needs
additional CLDR forms such as `_few` or `_many`.
## Key Features
### Placeholder Preservation