From dad2425c27aaa3c2fb553664671ac1d7e54602f1 Mon Sep 17 00:00:00 2001 From: Ludy Date: Sun, 14 Jun 2026 16:47:57 +0200 Subject: [PATCH 1/7] docs: Document pluralization suffix usage for translations (#6650) --- scripts/translations/README.md | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/scripts/translations/README.md b/scripts/translations/README.md index ed74c74e7..b078bb758 100644 --- a/scripts/translations/README.md +++ b/scripts/translations/README.md @@ -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 From 1ce765ab1e0aadf8d87d7df50757dd43fc9bc97f Mon Sep 17 00:00:00 2001 From: Ludy Date: Sun, 14 Jun 2026 16:48:28 +0200 Subject: [PATCH 2/7] refactor: replace legacy Paths usage with Path.of (#6441) --- .../common/configuration/AppConfig.java | 7 +- .../configuration/ConfigInitializer.java | 7 +- .../configuration/RuntimePathConfig.java | 11 +-- .../software/common/model/FileInfo.java | 3 +- .../common/service/MobileScannerService.java | 3 +- .../common/service/PostHogService.java | 4 +- .../software/common/util/GeneralUtils.java | 16 +-- .../software/common/util/JarPathUtil.java | 9 +- .../model/ApplicationPropertiesLogicTest.java | 9 +- .../software/SPDF/SPDFApplication.java | 5 +- .../SPDF/config/ExternalAppDepConfig.java | 2 +- .../SPDF/controller/api/UIDataController.java | 3 +- .../api/misc/PrintFileController.java | 3 +- .../pipeline/PipelineDirectoryProcessor.java | 5 +- .../api/pipeline/PipelineProcessor.java | 5 +- .../web/ReactRoutingController.java | 5 +- .../service/PdfJsonConversionService.java | 4 +- .../SPDF/service/SharedSignatureService.java | 21 ++-- .../type3/library/Type3FontLibrary.java | 3 +- .../type3/tool/Type3SignatureTool.java | 5 +- .../service/telegram/TelegramPipelineBot.java | 5 +- .../api/misc/PrintFileControllerTest.java | 6 +- .../api/AuditDashboardController.java | 6 +- .../controller/api/AuditRestController.java | 29 ++---- .../controller/api/SignatureController.java | 3 +- .../controller/api/UsageRestController.java | 3 +- .../configuration/ee/LicenseKeyChecker.java | 3 +- .../api/AdminLicenseController.java | 3 +- .../api/AdminSettingsController.java | 6 +- .../controller/api/InviteLinkController.java | 5 +- .../api/UIDataTessdataController.java | 5 +- .../controller/api/UserController.java | 2 +- .../security/service/DatabaseService.java | 24 ++--- .../service/KeyPairCleanupService.java | 3 +- .../service/KeyPersistenceService.java | 26 ++--- .../security/service/LoginAttemptService.java | 3 +- .../service/RefreshRateLimitService.java | 29 ++---- .../proprietary/service/AuditService.java | 3 +- .../service/ServerCertificateService.java | 3 +- .../proprietary/service/SignatureService.java | 19 ++-- .../storage/config/StorageProviderConfig.java | 5 +- .../provider/LocalStorageProvider.java | 3 +- .../storage/provider/S3StorageProvider.java | 6 +- .../storage/service/FileStorageService.java | 15 ++- .../controller/SigningSessionController.java | 2 +- .../service/SigningFinalizationService.java | 97 ++++++------------- .../service/WorkflowSessionService.java | 2 +- .../workflow/util/WorkflowMapper.java | 5 +- scripts/PropSync.java | 2 +- scripts/RestartHelper.java | 4 +- 50 files changed, 178 insertions(+), 279 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java b/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java index 6d5e2507e..ba896b4e3 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java @@ -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; diff --git a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java index 4b3c237ec..6d3f366b8 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java +++ b/app/common/src/main/java/stirling/software/common/configuration/ConfigInitializer.java @@ -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); diff --git a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java index b1ecc1020..82921c0ba 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/RuntimePathConfig.java @@ -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 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)) { diff --git a/app/common/src/main/java/stirling/software/common/model/FileInfo.java b/app/common/src/main/java/stirling/software/common/model/FileInfo.java index e89420296..0d2777c43 100644 --- a/app/common/src/main/java/stirling/software/common/model/FileInfo.java +++ b/app/common/src/main/java/stirling/software/common/model/FileInfo.java @@ -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. diff --git a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java index 49512af70..7c544b624 100644 --- a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java +++ b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java @@ -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); } diff --git a/app/common/src/main/java/stirling/software/common/service/PostHogService.java b/app/common/src/main/java/stirling/software/common/service/PostHogService.java index 92093762f..f6a339ab9 100644 --- a/app/common/src/main/java/stirling/software/common/service/PostHogService.java +++ b/app/common/src/main/java/stirling/software/common/service/PostHogService.java @@ -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 getDockerMetrics() { diff --git a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java index 61ba8670f..fbcf30fdf 100644 --- a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java @@ -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); diff --git a/app/common/src/main/java/stirling/software/common/util/JarPathUtil.java b/app/common/src/main/java/stirling/software/common/util/JarPathUtil.java index e5c6488eb..bb0e39491 100644 --- a/app/common/src/main/java/stirling/software/common/util/JarPathUtil.java +++ b/app/common/src/main/java/stirling/software/common/util/JarPathUtil.java @@ -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) { diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java index f075f1518..ea5f95d77 100644 --- a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java @@ -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; @@ -33,16 +33,15 @@ class ApplicationPropertiesLogicTest { @Test void tempFileManagement_defaults_and_overrides() { - Function normalize = s -> Paths.get(s).normalize().toString(); + Function 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"); diff --git a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java index 177d2443a..3cc310075 100644 --- a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -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 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 = diff --git a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java index c08e53e1a..8755dfe2e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -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 diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java index c1afe8c40..de391c7c3 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java @@ -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 paths = - Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) { + Files.walk(Path.of(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) { List jsonFiles = paths.filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".json")) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java index 48a9b5586..e369bbf10 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java @@ -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); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index ef0afce94..79223ddd3 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -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 filesToProcess, Path processingDir) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index fde2cfa90..c57a43f27 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -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 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)) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index d6a95e66d..62ce9dac0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -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()); } diff --git a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java index 1eed1d275..fea3b52fc 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java @@ -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( diff --git a/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java b/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java index b1321de3f..3f52dbea2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java @@ -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 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 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 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 stream = Files.list(sharedFolder)) { List matchingFiles = diff --git a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java index 0c885dfb2..457d2c9c8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java @@ -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) { diff --git a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/tool/Type3SignatureTool.java b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/tool/Type3SignatureTool.java index ff1836913..10bc39618 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/tool/Type3SignatureTool.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/tool/Type3SignatureTool.java @@ -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)) { diff --git a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java index 89910781b..3b8d5cff1 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java @@ -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 waitForPipelineOutputs(PipelineFileInfo info) throws IOException { - Path finishedDir = Paths.get(runtimePathConfig.getPipelineFinishedFoldersPath()); + Path finishedDir = Path.of(runtimePathConfig.getPipelineFinishedFoldersPath()); Files.createDirectories(finishedDir); Instant start = info.savedAt(); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/PrintFileControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/PrintFileControllerTest.java index 5fb8d4ce8..7fe102791 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/PrintFileControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/PrintFileControllerTest.java @@ -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()); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java index 837c28bf5..6e93ed14e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java @@ -207,16 +207,14 @@ public class AuditDashboardController { // Include standard enum types in case they're not in the database yet List 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 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. */ diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java index a208a46a8..69c940292 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditRestController.java @@ -118,7 +118,7 @@ public class AuditRestController { // Convert to response format expected by frontend List 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 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 combinedTypes = new HashSet<>(); combinedTypes.addAll(dbTypes); combinedTypes.addAll(enumTypes); - List result = combinedTypes.stream().sorted().collect(Collectors.toList()); + List 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 principalCounts = auditRepository.countByPrincipal(); - List users = - principalCounts.stream() - .map(arr -> (String) arr[0]) - .sorted() - .collect(Collectors.toList()); + List users = principalCounts.stream().map(arr -> (String) arr[0]).sorted().toList(); return ResponseEntity.ok(users); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java index 3295e62ab..5333ff18c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java @@ -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)) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/UsageRestController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/UsageRestController.java index 9f4fa470b..1230d928c 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/UsageRestController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/UsageRestController.java @@ -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) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java index af9ac192d..d8dbfc0e0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java @@ -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; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java index e9cd65f7c..9e05e9ebb 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminLicenseController.java @@ -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( diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java index 06fc80c75..14ddde602 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AdminSettingsController.java @@ -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 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; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/InviteLinkController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/InviteLinkController.java index 07ec1c973..641216e26 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/InviteLinkController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/InviteLinkController.java @@ -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 expiredInvites = inviteTokenRepository.findAll().stream() .filter(invite -> !invite.isValid()) - .collect(Collectors.toList()); + .toList(); int count = expiredInvites.size(); inviteTokenRepository.deleteAll(expiredInvites); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java index 743425aec..8212dcfb2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java @@ -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) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java index 90abd5f06..295e90029 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java @@ -994,7 +994,7 @@ public class UserController { userRepository.findAll().stream() .filter(User::isEnabled) .map(this::toUserSummaryDTO) - .collect(java.util.stream.Collectors.toList()); + .toList(); return ResponseEntity.ok(users); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java index f120e4e42..2246903d2 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java @@ -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 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 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); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPairCleanupService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPairCleanupService.java index aec455a92..8745f505f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPairCleanupService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPairCleanupService.java @@ -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)) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPersistenceService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPersistenceService.java index d0c9f879b..49e66e5c5 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPersistenceService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPersistenceService.java @@ -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 { *

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); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/LoginAttemptService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/LoginAttemptService.java index d715771b5..d3819c35d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/LoginAttemptService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/LoginAttemptService.java @@ -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) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/RefreshRateLimitService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/RefreshRateLimitService.java index bb4e45429..6c4321ed7 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/RefreshRateLimitService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/RefreshRateLimitService.java @@ -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 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); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java index 9f50b9f59..d86ae644e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java @@ -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); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java index 01db79a5d..44d750e8e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/ServerCertificateService.java @@ -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() { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java index 3b7c0ed5d..8d4dc52a9 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java @@ -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 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 diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java index e990fa2ce..db063cda8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/config/StorageProviderConfig.java @@ -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 diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/LocalStorageProvider.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/LocalStorageProvider.java index 75f9eb3fd..b67b66f54 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/LocalStorageProvider.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/LocalStorageProvider.java @@ -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; } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/S3StorageProvider.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/S3StorageProvider.java index a1582458b..2b88d6484 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/S3StorageProvider.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/provider/S3StorageProvider.java @@ -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; } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java index 03c3a9178..37d1e5fa4 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FileStorageService.java @@ -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 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 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 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) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java index 756fba1fe..2464b6ed6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java @@ -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); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/SigningFinalizationService.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/SigningFinalizationService.java index aac9c180f..dc8469dc8 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/SigningFinalizationService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/SigningFinalizationService.java @@ -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) {} } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java index e8887e2f5..dc22c4052 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/service/WorkflowSessionService.java @@ -553,7 +553,7 @@ public class WorkflowSessionService { dto.setMyStatus(p.getStatus()); return dto; }) - .collect(java.util.stream.Collectors.toList()); + .toList(); } /** diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/util/WorkflowMapper.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/util/WorkflowMapper.java index 6be7197d5..b2f53c282 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/util/WorkflowMapper.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/util/WorkflowMapper.java @@ -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 diff --git a/scripts/PropSync.java b/scripts/PropSync.java index 2c78f64c0..5fce1c33a 100644 --- a/scripts/PropSync.java +++ b/scripts/PropSync.java @@ -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 enLines = Files.readAllLines(Paths.get(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8); + List enLines = Files.readAllLines(Path.of(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8); Map enProps = linesToProps(enLines); for (File file : files) { diff --git a/scripts/RestartHelper.java b/scripts/RestartHelper.java index 322beb522..fe48dd703 100644 --- a/scripts/RestartHelper.java +++ b/scripts/RestartHelper.java @@ -19,9 +19,9 @@ public class RestartHelper { Map 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)) { From 1fa1293b3935106193cd9ee377c81c0c5ce75d3a Mon Sep 17 00:00:00 2001 From: Ludy Date: Sun, 14 Jun 2026 16:48:44 +0200 Subject: [PATCH 3/7] chore(build): upgrade Gradle wrapper and Docker build images to 9.5.1 (#6501) --- .../workflows/PR-Demo-Comment-with-react.yml | 2 +- .github/workflows/ai-engine.yml | 2 +- .github/workflows/backend-build.yml | 2 +- .github/workflows/check-licence.yml | 2 +- .github/workflows/check-openapi.yml | 2 +- .github/workflows/db-migration-test.yml | 2 +- .github/workflows/docker-compose-tests.yml | 2 +- .../frontend-backend-licenses-update.yml | 2 +- .github/workflows/multiOSReleases.yml | 6 +- .github/workflows/push-docker.yml | 2 +- .github/workflows/swagger.yml | 2 +- .github/workflows/tauri-build.yml | 2 +- .github/workflows/test-build-docker.yml | 2 +- .github/workflows/testdriver.yml | 2 +- .taskfiles/desktop.yml | 17 ++--- docker/embedded/Dockerfile | 2 +- docker/embedded/Dockerfile.fat | 2 +- docker/embedded/Dockerfile.ultra-lite | 2 +- .../editor/scripts/build-universal-mac-jre.sh | 9 ++- gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 2 +- gradlew.bat | 31 +++------ scripts/db-migration/run-migration-test.sh | 59 +++++++++++++++++- 24 files changed, 107 insertions(+), 53 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 4e72e57bf..519ad8975 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.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 diff --git a/.github/workflows/ai-engine.yml b/.github/workflows/ai-engine.yml index 9c701a5a6..46e0262a6 100644 --- a/.github/workflows/ai-engine.yml +++ b/.github/workflows/ai-engine.yml @@ -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 diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 8d3023a89..f90c42d2a 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -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 diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml index 400bdc94a..dd2475906 100644 --- a/.github/workflows/check-licence.yml +++ b/.github/workflows/check-licence.yml @@ -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 diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index c6d4731cd..984502474 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -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 diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index 2a386d1a5..114855c4b 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -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 diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index 69031fed5..add65d6a8 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -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 diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index 9c530944a..0eaab674e 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -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 diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 32c97022a..7d7600936 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -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 diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index c637b9158..6ee9f89f4 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -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 diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index 295498e57..31e66325c 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -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 diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index 82dc1b63f..f26f59efd 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -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 diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml index 9d9c8f2bf..f020e5a0e 100644 --- a/.github/workflows/test-build-docker.yml +++ b/.github/workflows/test-build-docker.yml @@ -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 diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 350d6c077..7d5036964 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -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 diff --git a/.taskfiles/desktop.yml b/.taskfiles/desktop.yml index 4cd4c017f..0b37df9f6 100644 --- a/.taskfiles/desktop.yml +++ b/.taskfiles/desktop.yml @@ -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//runtime/jre/...`, so the diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index 89913af09..1e65d0432 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -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 \ diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index f9754b472..3a33f9677 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -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 \ diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index d091d7b3f..dabddbc6f 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -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 diff --git a/frontend/editor/scripts/build-universal-mac-jre.sh b/frontend/editor/scripts/build-universal-mac-jre.sh index baf66016b..f459bf7b6 100755 --- a/frontend/editor/scripts/build-universal-mac-jre.sh +++ b/frontend/editor/scripts/build-universal-mac-jre.sh @@ -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" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39773 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%0D6K65^m-0x6bZ9rDjF>In@OH( z-}n(gbEW`7N;Uw_nvT;^Dka<4HW`~@d4Ar1vP6&Q8=(0P)eG+4q=iBy>>s6#LLaiI z8Ius$PjnUlOR@A0RT&$X@hAoJ70tH@R`t4bsi-gdvtDkF(|IT~X#VhfF}CW+LQ7FW zSCNg@0d5;A>uzYV4~NGgNQxY^mrkR5QK&vnGw3CnNw3furU7!AZlW^NZ8XbtJC&P% zUZvTlH)xLOZJKMIO)m3%nrB`_^Ubxiz~?+yp|iN&T=EwTkfQDE%!hMSq#BO znPfZowh7rr_-LTI6JC5QuwEpv41d*sTb<#r!bL{Oe)x&GLyY# zLIBZRGLO=gqKSY}SBMZm*Ua_TQ9^uh;QMA2wwOAK9o3H6%iT8%>4Q0Pe|TBUf%$0VOHR=*E~+%SzZrDd+t#Ea7=v2I9{w8WcjX}z#b;jQh&*4=4IZK>gAkr&I%Q-uf7#`b@v z445^+&wQ(+H4C;az4Zb~B9#ysl|-y|$yh#%^l+I5GKSgjYy2pU*>B>c zTd{C8PsNu@i6PRUvsF;PGHb-8HBi`z0ayr&YC?+eT?{WbUxFJB6jmX3I0_AI-2=7(f^3?W&}|!N zOYhPXc4q+M`adSf=X)-NHXLhyh%EbDTX3O48 zY-q^Lv~;AhxmYr3kdvwU>e!xGOEQ+))v~$wYBQcQ$j#(Vrg!Z!GfXFUR+!(Z2UjzB z`qFz-$#krbskRd1I(rzbGlpWhfwkGJIoO*N!HX*KUtG?ENej$<@nk-m*rPXpvo;<# zv)Qg#DyCICuUtYRl`}J`ShA%bj4jD@d^fDrvVPj>5bi!VkxJy&FkMl1f=!-qTW`FF z`b(J{b@i9}shZ~a$e*4GQ>Z9@Y41Ce8Aai{|^uLtgo|z)wD^l5eN@$xk!n z+o??R;3$dy;dO~@E|ciZi+^r^NvCs6o9a6C*(cI1vdrqv2~X(BiIc6aue3kgd6w9; zUA!9B^q0#rXc6pd?!%f{z5mPnw1k%WXfd5W$>pqt460)a=w9fTY-vv?lkh}nnl-3_ z$!iV{<%T;UjcHC@edh$H^sV+&n3{alNX=kPqDNG~y0h!*FQ2?cKb=W`%S4OiOtYqp z8FV&N&4lQ+nM_QtX;7<=R?5LuC9>-h8EBTy4EyOEYeNxiFxH0~5UA^%dY4wq#?Phm zWIh~i=48bUx`4?@WY^2?{M1HEt7t8?GhKC|HhHM8?94)EEX|$~>Pws1P%51Z#nY)= zERhPuQo~@gbV``n=Fs|oeM4xNeW6@B)SpQ8h0J}icrG~{T2|s4ZuZc6oKJr`GZ=$O zɵ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 Hv&Q@%MAWx6 delta 37584 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 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% diff --git a/scripts/db-migration/run-migration-test.sh b/scripts/db-migration/run-migration-test.sh index 953f58712..47aeea863 100644 --- a/scripts/db-migration/run-migration-test.sh +++ b/scripts/db-migration/run-migration-test.sh @@ -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" From cb131021172d88ca3721f0dbcdf6267c05236d00 Mon Sep 17 00:00:00 2001 From: Ludy Date: Sun, 14 Jun 2026 16:49:45 +0200 Subject: [PATCH 4/7] chore(frontend): make Vite allowed hosts configurable (#6354) --- frontend/editor/vite.config.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/editor/vite.config.ts b/frontend/editor/vite.config.ts index 0d5d47915..c46c224df 100644 --- a/frontend/editor/vite.config.ts +++ b/frontend/editor/vite.config.ts @@ -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, @@ -205,6 +218,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 From 085ad6c784ede36565452bee1fd4618ab727e086 Mon Sep 17 00:00:00 2001 From: Ludy Date: Sun, 14 Jun 2026 16:50:31 +0200 Subject: [PATCH 5/7] build(taskfile): use platform-specific Gradle wrapper command (#6355) Co-authored-by: James Brunton --- .github/config/.files.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 5343246c2..2d617f8f2 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -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 \ No newline at end of file + - .github/workflows/build-enterprise.yml From d6a5777c6993974dc044eca494497aef2f30dea2 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:09:40 +0100 Subject: [PATCH 6/7] Fix pgvector (#6591) --- engine/src/stirling/documents/pgvector_store.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/engine/src/stirling/documents/pgvector_store.py b/engine/src/stirling/documents/pgvector_store.py index 8aff5ad3a..bdaf43011 100644 --- a/engine/src/stirling/documents/pgvector_store.py +++ b/engine/src/stirling/documents/pgvector_store.py @@ -4,6 +4,7 @@ import json from datetime import datetime import psycopg +from pgvector import Vector from pgvector.psycopg import register_vector_async from stirling.contracts.documents import Page, PageRange @@ -45,6 +46,13 @@ class PgVectorStore(DocumentStore): async def _ensure_schema(self) -> None: if self._initialized: return + # The `vector` type must exist before register_vector_async (called inside + # _connect) can resolve it. On a fresh database it doesn't yet, so create the + # extension first on a raw connection that hasn't registered the type. + async with await psycopg.AsyncConnection.connect(self._dsn) as bootstrap: + async with bootstrap.cursor() as cur: + await cur.execute("CREATE EXTENSION IF NOT EXISTS vector") + await bootstrap.commit() async with await self._connect() as conn: async with conn.cursor() as cur: await cur.execute("CREATE EXTENSION IF NOT EXISTS vector") @@ -206,7 +214,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() @@ -283,6 +291,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 @@ -291,7 +300,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() From c55beaceaddf4cf01cc0bda77b591271eb6f8615 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Mon, 15 Jun 2026 14:20:25 +0100 Subject: [PATCH 7/7] Shut codespell up --- scripts/sync_en_us_spelling.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/sync_en_us_spelling.py b/scripts/sync_en_us_spelling.py index b16d1be1b..73c233ee3 100644 --- a/scripts/sync_en_us_spelling.py +++ b/scripts/sync_en_us_spelling.py @@ -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))