Update SaaS to latest main (#6667)

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

Fixes conflicts in `pgvector_store.py`. 

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

---------

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