This commit is contained in:
Anthony Stirling
2026-08-03 22:01:39 +01:00
647 changed files with 11903 additions and 4709 deletions
+2
View File
@@ -123,8 +123,10 @@ generated-models: &generated-models
- *openapi
- frontend/editor/scripts/generate-tool-api-types.mts
- frontend/editor/src/core/types/toolApiTypes.ts
- frontend/editor/src/core/types/toolIO.ts
- engine/scripts/generate_tool_models.py
- engine/src/stirling/models/tool_models.py
- engine/src/stirling/models/tool_io.py
- .taskfiles/frontend.yml
- .taskfiles/engine.yml
- .github/workflows/check-generated-models.yml
+1 -1
View File
@@ -331,7 +331,7 @@ jobs:
- name: Set up Node.js for Storybook
if: steps.sb-changes.outputs.storybook == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-suffix: ai-engine
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
java-version: "25"
distribution: "temurin"
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
+4 -2
View File
@@ -174,12 +174,14 @@ jobs:
pull-requests: write
uses: ./.github/workflows/tauri-build.yml
secrets: inherit
# PR smoke build: macOS + Windows (the platforms our developers use).
# PR smoke build: macOS + Windows (the platforms our developers use).
# sign: true only reaches macOS - tauri-build's per-platform gate keeps
# Windows/Linux signing on main, and an unsigned .dmg cannot be opened.
# The full signed multi-OS matrix runs on release;
# nightly still warms the Rust cache with all-OS defaults.
with:
platform: windows-macos
sign: false
sign: true
ai-engine:
if: needs.files-changed.outputs.engine == 'true'
+15 -24
View File
@@ -1,12 +1,12 @@
name: Check generated models
# Verifies the committed generated API models are still in sync with the Java
# OpenAPI spec: the frontend tool API types
# (frontend/editor/src/core/types/toolApiTypes.ts) and the engine tool
# models (engine/src/stirling/models/tool_models.py). Regenerates both with the
# single top-level `task tool-models` and fails if either committed file is
# out of date. Called from build.yml when the backend Java, frontend, or engine
# changes; also runs on push to main as a post-merge safety net.
# Verifies the committed generated files are still in sync with the Java OpenAPI
# spec: the request models (toolApiTypes.ts, tool_models.py) and the tool I/O
# tables saying what each endpoint accepts and produces (toolIO.ts, tool_io.py).
# Regenerates them all with the single top-level `task tool-models` and fails if
# any committed file is out of date. Called from build.yml when the
# backend Java, frontend, or engine changes; also runs on push to main as a
# post-merge safety net.
on:
workflow_call:
push:
@@ -31,7 +31,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-suffix: generated-models
@@ -48,7 +48,7 @@ jobs:
gradle-version: 9.6.0
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
@@ -57,18 +57,10 @@ jobs:
- name: Install Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
# Rebuilds the OpenAPI spec from the current Java and regenerates both the
# frontend types and the engine tool models from it.
- name: Regenerate generated models
run: task tool-models
- name: Verify generated models are up to date
id: models-check
continue-on-error: true
run: |
git diff --exit-code \
frontend/editor/src/core/types/toolApiTypes.ts \
engine/src/stirling/models/tool_models.py
run: task tool-models:check
- name: Comment on generated models check failure
# Only post a comment on PRs. github-script's PR helpers need an
@@ -83,9 +75,9 @@ jobs:
marker,
'### Generated Models Check Failed',
'',
'The generated `frontend/editor/src/core/types/toolApiTypes.ts` and/or `engine/src/stirling/models/tool_models.py` are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.',
'One or more generated files are out of date with the Java OpenAPI spec and will need to be regenerated before they can be merged in.',
'',
'Run `task tool-models` to regenerate both, then commit the updated files.',
'Run `task tool-models` to regenerate them, then commit the updated files.',
].join('\n');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
@@ -116,11 +108,10 @@ jobs:
echo " Generated Models Check Failed"
echo "============================================"
echo ""
echo "The generated frontend API types and/or engine tool"
echo "models are out of date with the Java OpenAPI spec and"
echo "will need to be regenerated before they can be merged in."
echo "One or more generated files are out of date with the Java"
echo "OpenAPI spec and will need to be regenerated before merging."
echo ""
echo "Run 'task tool-models' to regenerate both, then"
echo "Run 'task tool-models' to regenerate them, then"
echo "commit the updated files."
echo "============================================"
exit 1
+43 -1
View File
@@ -25,8 +25,41 @@ jobs:
with:
java-version: "25"
distribution: "temurin"
# Same cache layer as backend-build.yml. Without it every run resolved the
# whole classpath cold and eventually got HTTP 429 from Maven Central.
- name: Cache Gradle dependency artifacts
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.gradle/wrapper
~/.gradle/caches/modules-2/files-2.1
~/.gradle/caches/modules-2/metadata-2.*
key: gradle-deps-${{ runner.os }}-jdk-25-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/*.gradle', '**/*.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle/libs.versions.toml') }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
gradle-version: 9.6.1
cache-disabled: true
# Gradle does not retry 429s, and a cold cache resolving the buildscript
# classpath is exactly where Maven Central rate-limits us. Retry it here,
# where a failure is cheap, instead of inside the backgrounded bootRun.
- name: Prime Gradle dependencies
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
run: |
for attempt in 1 2 3; do
if ./gradlew --quiet -PnoSpotless :stirling-pdf:classes; then
exit 0
fi
echo "::warning::Gradle dependency resolution failed (attempt $attempt of 3)"
sleep $((attempt * 30))
done
echo "::error::Gradle could not resolve dependencies after 3 attempts"
exit 1
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
@@ -53,6 +86,11 @@ jobs:
# to aggregate. Chromium-only - other engines silently skip.
PW_COVERAGE: "1"
PLAYWRIGHT_JSON_OUTPUT_FILE: ${{ github.workspace }}/frontend/playwright-report/results.json
# Internal mirror, as in backend-build.yml. Empty on Dependabot and
# fork PRs, where the build falls back to Maven Central.
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
run: task e2e:live
- name: Flag flaky tests
# Runs regardless of the test outcome: a flaky test (passed on retry)
@@ -66,6 +104,10 @@ jobs:
- name: Generate JaCoCo report from e2e:live .exec
if: always()
id: live-coverage
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
# `if: always()` so even a failed test run still produces a
# report from whatever flows did exercise the backend before
# the failure. The task itself tolerates a missing .exec
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
# Need the base branch too, to diff against it.
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
@@ -80,7 +80,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
+2 -2
View File
@@ -146,7 +146,7 @@ jobs:
- name: Setup Node.js
if: matrix.variant.build_frontend == true
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: "npm"
@@ -208,7 +208,7 @@ jobs:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: "npm"
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
@@ -72,7 +72,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-suffix: pre-commit
+1 -1
View File
@@ -75,6 +75,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v3.29.5
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v3.29.5
with:
sarif_file: results.sarif
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: "npm"
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
cache-suffix: sync-files
+11 -6
View File
@@ -108,6 +108,11 @@ jobs:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
RELEASE_GPG_PRIVATE_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }}
# Per-platform sign gate. macOS signs on any run with the cert available,
# PRs included: Gatekeeper blocks an unsigned .dmg, so an unsigned macOS
# PR build is not testable. Windows and Linux stay main-only, matching the
# gates on their own signing steps below.
SIGN_BUNDLE: ${{ inputs.sign && (matrix.platform == 'macos-15' && secrets.APPLE_CERTIFICATE != '' || github.ref == 'refs/heads/main') }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
@@ -124,7 +129,7 @@ jobs:
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: "npm"
@@ -274,7 +279,7 @@ jobs:
}
- name: Import Apple Developer Certificate
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -295,7 +300,7 @@ jobs:
rm certificate.p12
- name: Verify Certificate
if: inputs.sign && matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
run: |
echo "Verifying Apple Developer Certificate..."
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
@@ -368,7 +373,7 @@ jobs:
fi
- name: Build Tauri app (signed)
if: inputs.sign
if: env.SIGN_BUNDLE == 'true'
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -403,7 +408,7 @@ jobs:
args: ${{ matrix.platform == 'ubuntu-22.04' && (inputs.minimal && '--bundles deb' || '--bundles deb,rpm') || matrix.args }}
- name: Build Tauri app (unsigned)
if: ${{ !inputs.sign }}
if: env.SIGN_BUNDLE != 'true'
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -458,7 +463,7 @@ jobs:
fi
- name: Verify notarization (macOS only)
if: inputs.sign && matrix.platform == 'macos-15'
if: env.SIGN_BUNDLE == 'true' && matrix.platform == 'macos-15'
run: |
echo "🔍 Verifying notarization status..."
cd ./frontend/editor/src-tauri/target
+1 -1
View File
@@ -156,7 +156,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: "npm"
cache-dependency-path: frontend/package-lock.json
+9
View File
@@ -114,6 +114,15 @@ tasks:
- cmd: ./gradlew test
platforms: [linux, darwin]
test:force:
desc: "Run backend tests, ignoring cached results"
aliases: [test:no-cache]
cmds:
- cmd: cmd /c ".\gradlew.bat cleanTest test --no-build-cache"
platforms: [windows]
- cmd: ./gradlew cleanTest test --no-build-cache
platforms: [linux, darwin]
format:
desc: "Auto-fix code formatting"
cmds:
+8 -1
View File
@@ -102,12 +102,19 @@ tasks:
desc: "Generate tool_models.py from Java OpenAPI spec (SwaggerDoc.json)"
deps: [install, ":backend:swagger"]
cmds:
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py
sources:
- ../SwaggerDoc.json
- scripts/generate_tool_models.py
generates:
- src/stirling/models/tool_models.py
- src/stirling/models/tool_io.py
tool-models:check:
desc: "Fail if the committed tool models are out of date"
deps: [install, ":backend:swagger"]
cmds:
- uv run python scripts/generate_tool_models.py --spec ../SwaggerDoc.json --output src/stirling/models/tool_models.py --io-output src/stirling/models/tool_io.py --check
clean:
desc: "Clean build artifacts"
+3 -2
View File
@@ -498,18 +498,19 @@ tasks:
desc: "Generate tool API types from the Java OpenAPI spec"
deps: [install, ":backend:swagger"]
cmds:
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts
sources:
- editor/scripts/generate-tool-api-types.mts
- ../SwaggerDoc.json
generates:
- editor/src/core/types/toolApiTypes.ts
- editor/src/core/types/toolIO.ts
tool-models:check:
desc: "Fail if committed tool API types are out of date"
deps: [install, ":backend:swagger"]
cmds:
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --check
- npx tsx editor/scripts/generate-tool-api-types.mts --spec ../SwaggerDoc.json --output editor/src/core/types/toolApiTypes.ts --io-output editor/src/core/types/toolIO.ts --check
licenses:generate:
desc: "Generate frontend license report"
+34 -1
View File
@@ -504,7 +504,8 @@ For Stirling 2.0, new features are built as React components:
1. **Create a New Controller:**
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory.
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates.")`.
- If the endpoint transforms a document, declare what it accepts and produces with `@ToolIO`, for example `@ToolIO(produces = ToolFormat.PDF)`. This is what lets a pipeline containing the step be checked before it runs, so a chain that cannot work is caught in the builder rather than part-way through a job. Endpoints under the tool namespaces are required to carry it - `ToolIODeclarationCoverageTest` fails the build otherwise. See [Declaring tool inputs and outputs](#declaring-tool-inputs-and-outputs).
```java
package stirling.software.SPDF.controller.api;
@@ -578,6 +579,38 @@ For Stirling 2.0, new features are built as React components:
}
```
### Declaring tool inputs and outputs
An endpoint that transforms a document declares what it accepts and produces with `@ToolIO`. This is the single source of truth: it is published into the OpenAPI spec as an `x-stirling-io` extension, and generated from there into the frontend (`toolIO.ts`) and the AI engine (`tool_io.py`). A pipeline can therefore be checked while it is being edited, instead of failing part-way through a job.
```java
@ToolIO(produces = ToolFormat.PDF)
```
`accepts` defaults to `{ ToolFormat.PDF }` and `arity` to `ToolArity.SISO`, so most tools only declare what they produce.
- **`ToolFormat`** is the kind of file: `PDF`, `PDF_ENCRYPTED`, `IMAGE`, `ZIP`, `WORD`, `PPT`, `EXCEL`, `CSV`, `HTML`, `XML`, `JSON`, `TEXT`, `MARKDOWN`, `JAVASCRIPT`, `EBOOK`, `EMAIL`, `POSTSCRIPT`, `VIDEO`, `CBZ`, `CBR`, plus `ANY` (accepts or produces anything) and `NONE` (returns a report, not a file). Encryption is a format rather than a flag, so the default `accepts = PDF` means an endpoint rejects an encrypted PDF unless it opts in.
- **`ToolArity`** is how many files go in and out: `SISO`, `SIMO`, `MISO`, `MIMO`. This axis carries ZIP-as-transport. A splitter is `produces = PDF, arity = SIMO`, and the caller unpacks the archive; an endpoint whose deliverable really is an archive declares `produces = ZIP` with a single-output arity and stays packed.
When the output depends on a parameter, declare the exception as a case rather than picking one answer. Add Password produces an encrypted PDF unless both passwords are blank, in which case it has only set permissions:
```java
@ToolIO(
produces = ToolFormat.PDF_ENCRYPTED,
cases =
@ToolIOCase(
when = {
@ToolIOWhen(param = "password", matches = ""),
@ToolIOWhen(param = "ownerPassword", matches = "")
},
produces = ToolFormat.PDF,
arity = ToolArity.SISO))
```
Every condition in a `when` must hold for the case to apply, and `matches` is compared as a string, case-insensitively, with an empty string matching an absent or blank value. If a case reads a parameter that is not set yet, the output is reported as uncertain and the chain warns rather than erroring.
Endpoints under the tool namespaces must carry a declaration; `ToolIODeclarationCoverageTest` fails the build for any that does not, with a short allowlist for endpoints that manage a session, a device or a stored resource rather than transforming a document. The matching rules are implemented three times (Java `ToolChainValidator`, `toolIOCompat.ts`, `tool_io_compat.py`) and pinned to the same answers by the shared fixtures in `testing/tool-io-cases.json`, so a behaviour change belongs in that file first.
## Adding New Translations to Existing Language Files in Stirling-PDF
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
+668
View File
@@ -0,0 +1,668 @@
# Quarkus migration TODO
The Spring Boot -> Quarkus port of this branch is not finished. This file is the single backlog for
what is left. The per-file `TODO: Migration required` comments that used to carry this information
have been removed from the source and folded in here. Companion docs:
`QUARKUS_MIGRATION_HANDOFF.md` (how to work on the migration - stack, commands, patterns) and
`migration-report.md` (the original summary).
## Status
| Module | `compileJava` | Notes |
|---|---|---|
| `:common` | clean | no Spring imports left in main sources |
| `:stirling-pdf` (`app/core`) | clean | no Spring imports left in main sources |
| `:proprietary` | **fails** | see [Blocking compile errors](#blocking-compile-errors) |
| `:saas` | not measured | opt-in flavor (`STIRLING_FLAVOR=saas`), sits on top of `:proprietary` |
`:proprietary` is compiled by **every** flavor - `settings.gradle` always includes it and only
`:saas` is conditional - so `./gradlew build` cannot pass on any flavor, `core` included, until the
list below is empty.
## Blocking compile errors
`STIRLING_FLAVOR=proprietary ./gradlew :proprietary:compileJava` reports **1266 errors across
70 files**. javac stops printing after 100 errors by default; to see them all, temporarily
add `options.compilerArgs << '-Xmaxerrs' << '20000'` to the `JavaCompile` tasks.
Two things worth knowing before working through this list:
- **Lombok bails on the first hard javac error and takes every generated member with it.** One
malformed annotation used to produce ~5500 extra `cannot find symbol: variable log` /
`cannot find symbol: method getX()` errors in unrelated files. If the count jumps, look for a
single real error first rather than trusting the total.
- Fix the shared types (repositories, resolvers) before their callers; a lot of `cannot find symbol`
in a controller is really its repository failing to compile.
Grouped by the port each file needs, largest group first.
### Spring MVC REST controllers -> JAX-RS (16 files, 682 errors)
`@RestController` + `@RequestMapping` -> `@ApplicationScoped` + `@Path`; `@GetMapping`/`@PostMapping`/`@PutMapping`/`@DeleteMapping` -> `@GET`/`@POST`/`@PUT`/`@DELETE` + `@Path`; `ResponseEntity<T>` -> `jakarta.ws.rs.core.Response`; `@RequestBody` -> a plain parameter; `@RequestParam` -> `@QueryParam` (or `@RestForm` for multipart); `@PathVariable` -> `@PathParam`; `@RequestHeader` -> `@HeaderParam`; `@ModelAttribute`/`@RequestPart` -> `@BeanParam`/`@RestForm`; `@AuthenticationPrincipal` -> injected `SecurityIdentity`. Exemplar: `controller/api/AdminJobController.java`.
- [ ] `proprietary/policy/controller/PolicyController.java` - 130 errors
- [ ] `proprietary/integration/controller/IntegrationConfigController.java` - 76 errors
- [ ] `proprietary/integration/api/ExternalApiCallController.java` - 68 errors
- [ ] `proprietary/policy/source/SourceController.java` - 64 errors
- [ ] `proprietary/access/controller/ResourceGrantController.java` - 54 errors
- [ ] `proprietary/accountlink/AccountLinkController.java` - 50 errors
- [ ] `proprietary/integration/purview/PurviewLabelController.java` - 48 errors
- [ ] `proprietary/security/controller/api/AdminLoginAgreementController.java` - 34 errors
- [ ] `proprietary/controller/api/PortalApiKeysController.java` - 32 errors
- [ ] `proprietary/policy/webhook/WebhookReceiverController.java` - 32 errors
- [ ] `proprietary/policy/controller/ClassificationMeterController.java` - 26 errors
- [ ] `proprietary/controller/api/ClassifyLabelController.java` - 24 errors
- [ ] `proprietary/controller/api/PortalDocumentsController.java` - 14 errors
- [ ] `proprietary/controller/api/PortalInfraAuditController.java` - 14 errors
- [ ] `proprietary/controller/api/FleetUsageController.java` - 12 errors
- [ ] `proprietary/policy/controller/FolderAccessSettingsController.java` - 4 errors
### Spring Data JPA repositories -> Panache (14 files, 328 errors)
`extends JpaRepository<T, ID>` -> `implements PanacheRepositoryBase<T, ID>` on an `@ApplicationScoped` class; derived finders become explicit `find`/`list` calls; `@Query`/`@Modifying`/`@Param` become Panache `find(...)`/`update(...)` with `Parameters.with(...)`; `findById` -> `findByIdOptional`; add a `save()` shim where callers expect Spring Data's. Exemplars: `policy/store/PolicyRepository.java`, `repository/PersistentAuditEventRepository.java`.
- [ ] `proprietary/policy/ledger/ProcessedFileRepository.java` - 136 errors
- [ ] `proprietary/security/repository/ApiKeyDailyUsageRepository.java` - 42 errors
- [ ] `proprietary/policy/source/SourceDocCountRepository.java` - 38 errors
- [ ] `proprietary/accountlink/UsageCounterRepository.java` - 34 errors
- [ ] `proprietary/policy/source/SourceDocTotalRepository.java` - 22 errors
- [ ] `proprietary/access/repository/ResourceGrantRepository.java` - 16 errors
- [ ] `proprietary/policy/source/SourceRepository.java` - 12 errors
- [ ] `proprietary/integration/repository/IntegrationConfigRepository.java` - 4 errors
- [ ] `proprietary/security/repository/ApiKeyRepository.java` - 4 errors
- [ ] `proprietary/accountlink/AccountLinkSyncStateRepository.java` - 4 errors
- [ ] `proprietary/accountlink/DeviceCredentialRepository.java` - 4 errors
- [ ] `proprietary/accountlink/MeteredInputSignatureRepository.java` - 4 errors
- [ ] `proprietary/policy/migration/CompletedMigrationRepository.java` - 4 errors
- [ ] `proprietary/security/repository/JwtSigningKeyRepository.java` - 4 errors
### Spring MVC infrastructure (interceptors, WebMvcConfigurer) (3 files, 34 errors)
`HandlerInterceptor` / `WebMvcConfigurer` / `HandlerMapping` / `WebUtils` have no Quarkus equivalent. Re-implement as a JAX-RS `@Provider ContainerRequestFilter` (or a Vert.x route filter for non-JAX-RS paths) and delete the MVC registration class.
- [ ] `proprietary/accountlink/InstanceEntitlementInterceptor.java` - 20 errors
- [ ] `proprietary/accountlink/AccountLinkWebMvcConfig.java` - 12 errors
- [ ] `proprietary/policy/controller/PolicyRunRoutes.java` - 2 errors
### ResponseStatusException -> WebApplicationException (4 files, 26 errors)
`ResponseStatusException(HttpStatus.X, msg)` -> `jakarta.ws.rs.WebApplicationException(msg, Response.Status.X)`; `HttpStatus` -> `Response.Status`.
- [ ] `proprietary/access/service/OwnershipService.java` - 10 errors
- [ ] `proprietary/integration/service/IntegrationConfigService.java` - 8 errors
- [ ] `proprietary/service/AiFeatureGate.java` - 4 errors
- [ ] `proprietary/security/service/ApiKeyManagementService.java` - 4 errors
### Conditional beans (9 files, 56 errors)
`@ConditionalOnProperty` / `@ConditionalOnMissingBean` gate on runtime config, which Arc cannot do at build time. Branch convention: keep the bean unconditional and guard at the call site on `ApplicationProperties`, or use `@io.quarkus.arc.lookup.LookupIfProperty` / `@IfBuildProfile` once the flag can become build-time.
- [ ] `proprietary/accountlink/UsageSyncService.java` - 14 errors
- [ ] `proprietary/access/config/AccessConfig.java` - 10 errors
- [ ] `proprietary/accountlink/DeviceCredentialStore.java` - 8 errors
- [ ] `proprietary/accountlink/AccountLinkClient.java` - 4 errors
- [ ] `proprietary/accountlink/AccountLinkService.java` - 4 errors
- [ ] `proprietary/accountlink/LocalUsageService.java` - 4 errors
- [ ] `proprietary/accountlink/EntitlementCache.java` - 4 errors
- [ ] `proprietary/accountlink/InstanceEntitlementGate.java` - 4 errors
- [ ] `proprietary/accountlink/UsageMeterService.java` - 4 errors
### Spring lifecycle events (5 files, 52 errors)
`@EventListener(ApplicationReadyEvent|ContextRefreshedEvent)` -> `void onStart(@Observes StartupEvent ev)`; `@TransactionalEventListener` -> an explicit call after the transaction commits; `ApplicationEventPublisher` -> CDI `Event<T>`.
- [ ] `proprietary/policy/seed/DefaultClassificationPolicySeeder.java` - 16 errors
- [ ] `proprietary/policy/output/PolicyInlineOutputMigration.java` - 10 errors
- [ ] `proprietary/policy/s3/EmbeddedS3CredentialMigration.java` - 10 errors
- [ ] `proprietary/policy/ledger/JpaProcessedLedger.java` - 8 errors
- [ ] `proprietary/service/AiEngineConfigSync.java` - 8 errors
### Paging / sorting / Persistable (4 files, 24 errors)
`Pageable`/`PageRequest`/`Sort`/`Page<T>` -> Panache `page(Page.of(n, size))` + `io.quarkus.panache.common.Sort`; `Persistable` -> drop it (Panache decides insert vs update from the id).
- [ ] `proprietary/service/PortalAuditReadService.java` - 12 errors
- [ ] `proprietary/policy/ledger/ProcessedFileEntity.java` - 4 errors
- [ ] `proprietary/policy/source/SourceDocCountEntity.java` - 4 errors
- [ ] `proprietary/policy/source/SourceDocTotalEntity.java` - 4 errors
### Scheduling (1 files, 4 errors)
`@Scheduled` / `SchedulingConfigurer` / `FixedDelayTask` -> `io.quarkus.scheduler.Scheduled(every = "...")`; a dynamic registrar becomes a `@Scheduled` method reading its interval from config.
- [ ] `proprietary/security/service/ApiKeyUsageRecorder.java` - 4 errors
### Transaction propagation (2 files, 20 errors)
`@Transactional(propagation = REQUIRES_NEW)` -> `jakarta.transaction.Transactional(REQUIRES_NEW)` or `QuarkusTransaction.requiringNew()`; `readOnly` has no jakarta equivalent and is dropped.
- [ ] `proprietary/security/service/ApiKeyUsageWriter.java` - 14 errors
- [ ] `proprietary/security/service/ApiKeyLegacyMigrator.java` - 6 errors
### Spring HTTP MediaType (2 files, 6 errors)
`org.springframework.http.MediaType` / `MediaTypeFactory` -> `jakarta.ws.rs.core.MediaType` plus an explicit extension-to-type map.
- [ ] `proprietary/policy/output/S3OutputSink.java` - 4 errors
- [ ] `proprietary/integration/api/ApiTokenCache.java` - 2 errors
### Remaining assorted Spring types (10 files, 34 errors)
Assorted remaining Spring types; each file's imports name what it needs.
- [ ] `proprietary/security/service/TeamMembershipService.java` - 8 errors
- [ ] `proprietary/model/TeamEntityListener.java` - 6 errors
- [ ] `proprietary/policy/input/S3InputSource.java` - 4 errors
- [ ] `proprietary/policy/source/JpaSourceDocCounter.java` - 4 errors
- [ ] `proprietary/accountlink/AccountLinkProperties.java` - 2 errors
- [ ] `proprietary/integration/api/ResultFiles.java` - 2 errors
- [ ] `proprietary/access/service/ResourceAccessService.java` - 2 errors
- [ ] `proprietary/access/security/ResourceAccessSecurity.java` - 2 errors
- [ ] `proprietary/integration/api/ApiConnectionResolver.java` - 2 errors
- [ ] `proprietary/policy/s3/S3ConnectionResolver.java` - 2 errors
## Deferred behaviour
374 notes were removed from the source and recorded here. These are places that compile
but where the behaviour is a stub, a fallback, or a Spring feature that was dropped rather than
ported - so they will not show up in a build and need reading before anyone trusts the
corresponding feature. Grouped by concern.
<details><summary><b>Spring MVC handler registry has no Quarkus equivalent</b> (8)</summary>
- `app/common/src/main/java/stirling/software/common/config/swagger/ToolIOOperationCustomizer.java:38` - GlobalOpenApiCustomizer}, which received the {@code HandlerMethod} for each operation and could read {@code @ToolIO} straight off it. A MicroProfile {@link OASFilter} sees only the document, so the declarations are looked up by path through {@link ToolIORegistry}. That registry is only populated once the container is up, hence {@code RUNTIME_STARTUP} - the schema exported at build time by {@code quarkus.smallrye-openapi.store-schema-directory} therefore carries no {@code x-stirling-io}. TODO:...
- `app/core/src/main/java/stirling/software/SPDF/config/EndpointInspector.java:32` - TODO: Migration required - this previously used Spring MVC's RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.*) to enumerate all registered GET handler mappings via the ApplicationContext at ContextRefreshedEvent. Quarkus/JAX-RS (RESTEasy Reactive) has no equivalent runtime-queryable handler-mapping registry. Options for porting: - Build-time scan of @jakarta.ws.rs.Path + @jakarta.ws.rs.GET via a Quarkus build step / Jandex index, or - Query the OpenAPI model (quarkus...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:85` - TODO: Migration required - endpoint discovery relied on Spring MVC's RequestMappingHandlerMapping (ApplicationContext.getBeansOfType(...) -> mapping.getHandlerMethods()) to enumerate every @RequestMapping/@PostMapping handler, its URL patterns (RequestMappingInfo#getDirectPaths), its HTTP methods (RequestMethod POST/PUT), and the HandlerMethod/MethodParameter reflection used to build request schemas. Quarkus/RESTEasy Reactive has no equivalent runtime registry of JAX-RS resources. To restore ...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:113` - TODO: Migration required - request body type was previously resolved from Spring's HandlerMethod#getMethodParameters(); resolve the first complex parameter type via plain reflection on the JAX-RS resource method instead, then call schemaGenerator.toSchema(...).
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/OperationMeta.java:16` - TODO: Migration required - was org.springframework.web.method.HandlerMethod (Spring MVC, no Quarkus equivalent). Replaced with the underlying java.lang.reflect.Method. The collaborator McpToolCatalog must be updated to discover JAX-RS resource methods (e.g. via RESTEasy Reactive ResourceScanningSupport / jakarta.ws.rs annotations) instead of Spring's RequestMappingHandlerMapping, and pass a reflect.Method here.
- `app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineEndpointResolver.java:44` - TODO: Migration required - this previously enumerated all registered request mappings via Spring MVC's RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.*) obtained from the ApplicationContext at ContextRefreshedEvent, keeping every pattern that started with "/api/v1/". Quarkus / JAX-RS (RESTEasy Reactive) has no equivalent runtime-queryable handler-mapping registry. Options for porting: - Build-time scan of @jakarta.ws.rs.Path methods via a Quarkus build step / Jandex ...
- `app/saas/src/main/java/stirling/software/saas/payg/entitlement/EntitlementGuard.java:366` - TODO: Migration required - resolves the resource {@link Method} the original code read from Spring's {@code HandlerMethod}. Until wired to JAX-RS {@code ResourceInfo}, supports a handler that is already a {@link Method} or exposes a no-arg {@code getMethod()} returning one.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:563` - TODO: Migration required - resolves the resource {@link Method} the original code read from Spring's {@code HandlerMethod} (via {@code hm.getMethod()}). Until wired to JAX-RS {@code ResourceInfo}, supports a handler that is already a {@link Method} or exposes a no-arg {@code getMethod()} returning one, preserving the {@code @AutoJobPostMapping} gating.
</details>
<details><summary><b>Servlet filters / interceptors -> JAX-RS providers</b> (108)</summary>
- `app/common/build.gradle:11` - Servlet bridge: large amounts of controller/filter code use jakarta.servlet (HttpServletRequest, Filter, etc.). quarkus-undertow provides a servlet container on Quarkus so that API resolves and runs. TODO: Migration required - longer term, port servlet usage to JAX-RS (ContainerRequestContext) and drop quarkus-undertow.
- `app/common/build.gradle:27` - REMOVED: spring-boot-starter-aspectj. Quarkus has no AspectJ weaving; quarkus-arc provides CDI interceptors (@AroundInvoke / interceptor bindings) instead. TODO: Migration required - any @Aspect/@Around advice must be rewritten as CDI interceptors.
- `app/core/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java:11` - TODO: Migration required - this class was a Spring MVC WebMvcConfigurer. Quarkus/JAX-RS has no WebMvcConfigurer, InterceptorRegistry, LocaleChangeInterceptor or SessionLocaleResolver. The locale-resolution logic (computing the default Locale from configuration) is preserved below as a CDI-produced Locale. The two pieces of behavior that previously came from the MVC machinery still need to be wired up by collaborators: 1. The "lang" request-param locale switching (old LocaleChangeInterceptor) ...
- `app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java:38` - are read automatically, so this class is now an {@link OASFilter} (registered via {@code mp.openapi.filter} in application.properties) that reproduces the old programmatic customizations: <ul> <li>API {@link Info} (title, version, license, contact, terms of service, description); <li>the global "AI" {@link Tag}; <li>the {@link Server} entry (optionally from {@code SWAGGER_SERVER_URL}); <li>the {@code ErrorResponse} component schema; <li>the {@code apiKey} security scheme + requirement when lo...
- `app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java:3` - TODO: Migration required - springdoc's GroupedOpenApi (multiple OpenAPI documents grouped by path-matching) has NO direct equivalent in quarkus-smallrye-openapi, which serves a single document built automatically from @Tag/@Operation/JAX-RS annotations. The three groups below (file-processing "/api/v1/**" minus management/system paths, management "/api/v1/admin/**" etc., and system "/api/v1/ui-data/**" etc.) plus the pdfFileOneOfCustomizer (@Qualifier("pdfFileOneOfCustomizer") OpenApiCustomiz...
- `app/core/src/main/java/stirling/software/SPDF/config/WAUTrackingFilter.java:21` - TODO: Migration required - Spring @ConditionalOnProperty(name="security.enableLogin", havingValue="false") had no direct CDI equivalent for conditional bean registration. The filter is now always registered (@Provider) and the condition is enforced at request time by reading the 'security.enableLogin' config property below. Verify the property key matches Quarkus config (originally bound from ApplicationProperties.security.enableLogin).
- `app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java:63` - TODO: Migration required - in Spring, addResourceHandlers also registered the physical resource locations (InstallationPathConfig.getStaticPath() + "classpath:/static/") and an EncodedResourceResolver (gzip/brotli pre-compressed asset serving). In Quarkus, static file serving is handled by quarkus.http via configuration: quarkus.http.static-resources... and/or a Servlet/RouteFilter mapping InstallationPathConfig.getStaticPath() as an external static root. The EncodedResourceResolver behavior ...
- `app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java:152` - TODO: Migration required - Quarkus has built-in CORS handling via quarkus.http.cors.* config properties (quarkus.http.cors.origins, .methods, .headers, .exposed-headers, .access-control-allow-credentials, .access-control-max-age). However, the original logic is *dynamic* (Tauri-mode detection + ApplicationProperties-driven origins + always-on Tauri origins), which static config cannot express. The logic is preserved below and applied via this response filter. Note: a ContainerResponseFilter c...
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:106` - TODO: Migration required - the per-request locale used to come from Spring's LocaleContextHolder (populated by the MVC LocaleChangeInterceptor). Until the equivalent ContainerRequestFilter described in LocaleConfiguration is in place, fall back to the JVM default locale. Localized messages are read from the shared messages.properties bundle (the same bundle ExceptionUtils uses) instead of a Spring MessageSource bean, which no longer exists under Quarkus.
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:806` - TODO: Migration required - the original Spring handler checked HttpServletResponse.isCommitted() and returned null to let Spring write nothing when the response was already committed (e.g. during streaming). JAX-RS ExceptionMapper has no direct access to commit state; returning a Response here is the closest equivalent. If streaming endpoints need the old "do nothing when committed" behavior, a collaborator should detect that condition (e.g. via a ContainerResponseFilter) and short-circuit.
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:873` - TODO: Migration required - locale is the JVM default until the per-request locale ContainerRequestFilter described in LocaleConfiguration replaces Spring's LocaleContextHolder.getLocale().
- `app/core/src/main/resources/application.properties:46` - TODO: Migration required - no direct Quarkus equivalent for the following; handle in code: - spring.threads.virtual.enabled=true -> annotate blocking endpoints with @RunOnVirtualThread - spring.mvc.async.request-timeout -> per-endpoint timeout handling - spring.security.filter.dispatcher-types=REQUEST,ERROR - spring.web.resources.mime-mappings.webmanifest=application/manifest+json - server.servlet.session.tracking-modes=cookie (configure on quarkus-undertow)
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:34` - {@code @Around("@annotation(...Audited)")} advice. Reworked into a CDI {@link Interceptor} bound by the {@code @Audited} annotation; {@code @Around}/{@code ProceedingJoinPoint} became {@code @AroundInvoke}/{@link InvocationContext}. Spring's {@code @Order(10)} (lower precedence, runs after {@code AutoJobAspect}) maps to {@code @Priority}: {@code AutoJobAspect} uses {@code @Priority(20)}, so this audit interceptor uses {@code @Priority(10)} which runs FIRST and populates MDC before the job int...
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:41` - stirling.software.proprietary.audit.Audited}) must be made a CDI {@code @jakarta.interceptor.InterceptorBinding} (and its members marked {@code @jakarta.enterprise.util.Nonbinding}) for this {@code @Interceptor} to bind to it; see the already-migrated {@code AutoJobPostMapping}. That is a separate file and is intentionally left untouched here. TODO: Migration required - {@code AuditService}'s helper methods ({@code createBaseAuditData}, {@code addFileData}, {@code addMethodArguments}, {@code ...
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:38` - multiple {@code @Around} advices whose pointcuts matched <em>any</em> method annotated with Spring's {@code @GetMapping}/{@code @PostMapping}/{@code @PutMapping}/{@code @DeleteMapping}/ {@code @PatchMapping}/{@code @AutoJobPostMapping}, plus an {@code execution(...)} expression on Spring's {@code ResourceHttpRequestHandler}. {@code @Around}/{@code ProceedingJoinPoint} + {@code MethodSignature} became {@code @AroundInvoke}/{@link InvocationContext}, and {@code RequestContextHolder}/{@code Serv...
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:204` - TODO: Migration required (collaborator) - AuditService.createBaseAuditData/addFileData/ addMethodArguments/resolveEventType still take org.aspectj.lang.ProceedingJoinPoint (AuditService is not yet migrated). Once AuditService is converted, change those signatures to accept jakarta.interceptor.InvocationContext (getMethod/getParameters/ getTarget cover the data used). These calls pass the InvocationContext and will only typecheck after that collaborator change. Use auditService to create the b...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpApiKeyAuthFilter.java:42` - TODO: Migration required - Spring Security removed. This filter previously read the current Authentication from SecurityContextHolder to decide whether to process the API key. Quarkus has no SecurityContextHolder; the current identity is exposed via io.quarkus.security.identity.SecurityIdentity. With the binding below not yet wired, we always attempt to validate the presented key so the lookup logic is preserved.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpApiKeyAuthFilter.java:51` - TODO: Migration required - bind the resolved user + MCP_SCOPES to the request identity. Spring's UsernamePasswordAuthenticationToken / SecurityContextHolder.setContext(...) has no servlet-filter equivalent in Quarkus. Implement an io.quarkus.security.identity.SecurityIdentityAugmentor (or a custom io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism / IdentityProvider keyed off the X-API-KEY / Bearer credential) that produces a SecurityIdentity with principal=user.getUsername() ...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpAudienceValidator.java:14` - RFC 8707 audience binding: a JWT at the MCP endpoint must list this server's resource id (or one of the explicitly accepted additional audiences) in its {@code aud} claim. The additional list exists for IdPs that cannot mint resource-specific audiences - e.g. Supabase's OAuth server always issues {@code aud=authenticated}. Fails closed when nothing is configured. TODO: Migration required - this was a Spring Security {@code OAuth2TokenValidator<Jwt>}. Quarkus-oidc has no equivalent validator S...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpAuthenticationEntryPoint.java:17` - Emits 401 + {@code WWW-Authenticate: Bearer resource_metadata="..."} (RFC 9728) from X-Forwarded-* headers. A rejected token also logs the reason and echoes it as {@code error_description}. TODO: Migration required - this was a Spring Security {@code AuthenticationEntryPoint} (commence(...) invoked by the SecurityFilterChain on authentication failure). Quarkus has no SecurityFilterChain equivalent. The 401 response must instead be produced by a Quarkus auth mechanism / failure handler (e.g. a...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpRequestSizeFilter.java:27` - TODO: Migration required - this filter was a Spring OncePerRequestFilter; under Quarkus (quarkus-undertow) register it as a jakarta.servlet.Filter via @WebFilter or a programmatic FilterRegistrationBean equivalent, and ensure it runs once per request and before the MCP endpoint. Registration ordering must be verified by the collaborator wiring the servlet filters.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:15` - MCP security chain: validates JWTs (JWKS + RFC 8707 audience), maps scope claims to authorities, and fails closed when the issuer is unset. TODO: Migration required - this class was a Spring Security {@code SecurityFilterChain} / {@code HttpSecurity} DSL configuration, which has NO direct Quarkus equivalent. The Spring security DSL has been removed; the equivalent behaviour must be rebuilt on Quarkus primitives: <ul> <li>HTTP path matching ({@code /mcp}, {@code /mcp/**}, {@code /.well-known/o...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:57` - TODO: Migration required - @Order(Ordered.HIGHEST_PRECEDENCE) and @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") were removed. Gate MCP security wiring on the runtime property mcp.enabled=true (a runtime toggle, not a build profile, so prefer a runtime guard in the new ContainerRequestFilter/augmentor). Filter ordering (highest precedence) must be re-expressed via JAX-RS @Priority or quarkus.http.auth.permission ordering.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:66` - TODO: Migration required - UserService was injected @Lazy to break a circular wiring with the security chain. With the Spring chain removed, inject it directly into the new API-key / user-binding ContainerRequestFilters instead of holding it here.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:26` - Binds an MCP-validated JWT to a provisioned Stirling user: optionally rejects subjects with no enabled account, then rebinds the principal to the canonical Stirling username (scope authorities only) so audit/metering attribute correctly. TODO: Migration required - this was a Spring Security {@code OncePerRequestFilter} that read and rewrote the {@code SecurityContextHolder} ({@code JwtAuthenticationToken}/{@code Jwt}). Quarkus has no global mutable security context; the canonical replacement ...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:60` - TODO: Migration required - extract the validated JWT and its claims from the Quarkus SecurityIdentity / JsonWebToken instead of Spring's SecurityContextHolder. The block below preserves the original binding logic but cannot run until that wiring exists, so for now every request passes through untouched.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:66` - TODO: Migration required - read the claim value from the validated token, e.g. jsonWebToken.getClaim(usernameClaim). Placeholder keeps the surrounding logic intact.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:98` - TODO: Migration required - rebind to the Stirling username, carrying only the OAuth scope authorities. With quarkus-oidc/smallrye-jwt this is done by a SecurityIdentityAugmentor that returns a new SecurityIdentity whose principal name is boundUsername and whose roles are the original token scopes. boundUsername is computed above and ready to feed into that augmentor.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:116` - TODO: Migration required - on the Quarkus path, rejection should clear/deny the SecurityIdentity (augmentor throws AuthenticationFailedException) or the ContainerRequestFilter should abortWith(Response.status(403)...). The 403 JSON body below is preserved as the intended response shape.
- `app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:333` - --------------------------------------------------------------------- Multi-value queries for filtering by multiple types and/or principals TODO: Migration required - callers must adapt to the PanacheQuery return type (see class doc). ---------------------------------------------------------------------
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:21` - TODO: Migration required - this class extended Spring Security's SimpleUrlAuthenticationFailureHandler and was wired into the form-login SecurityFilterChain. Quarkus has no direct equivalent for an AuthenticationFailureHandler. The login-failure flow (lockout, bad credentials, oauth2 errors, disabled users) must be re-hosted on a Quarkus authentication mechanism - typically a custom form-auth (quarkus.http.auth.*) or quarkus-oidc - with the redirect decisions implemented in a jakarta.ws.rs.co...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:24` - TODO: Migration required - this class previously extended Spring Security's SavedRequestAwareAuthenticationSuccessHandler, which is part of the Spring Security form-login filter chain (RedirectStrategy + SavedRequest from the HttpSession). Quarkus has no direct equivalent: post-login redirects are handled by quarkus-oidc / form-auth (quarkus.http.auth.form.landing-page, .location-cookie) or by a custom jakarta.servlet.Filter / ContainerRequestFilter / HttpAuthenticationMechanism. The business...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:89` - TODO: Migration required - "SPRING_SECURITY_SAVED_REQUEST" was populated by the Spring Security RequestCache. Without the Spring filter chain this attribute is never set, so this branch always falls through to the home-page redirect. The original-destination redirect must be reimplemented via the Quarkus form-auth location cookie or a custom request cache.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/JwtAuthenticationEntryPoint.java:9` - TODO: Migration required - this was a Spring Security AuthenticationEntryPoint (org.springframework.security.web.AuthenticationEntryPoint). Quarkus has no direct AuthenticationEntryPoint SPI; unauthenticated-access handling is wired via quarkus.http.auth.* policies and an AuthenticationFailedException mapper / a jakarta.ws.rs.ext.ExceptionMapper<io.quarkus.security.UnauthorizedException> (or a ContainerRequestFilter). The response-shaping logic below is preserved as a plain helper bean; the c...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpointAspect.java:23` - MIGRATION (Spring AOP -> CDI interceptor): was an {@code @Aspect} {@code @Component} with {@code @Around} advice matching {@code @annotation(EnterpriseEndpoint)} / {@code @within(EnterpriseEndpoint)}. Reworked into a CDI {@link Interceptor} bound by the {@code @EnterpriseEndpoint} annotation (pattern: common/aop/AutoJobAspect). {@code @Around} + {@code ProceedingJoinPoint} became {@code @AroundInvoke} + {@link InvocationContext}; {@code joinPoint.proceed()} -> {@code ctx.proceed()}. The Sprin...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/config/PremiumEndpointAspect.java:20` - MIGRATION (Spring AOP -> CDI interceptor): was an {@code @Aspect} with {@code @Around} advice on the {@code @PremiumEndpoint} pointcut ({@code @annotation || @within}). Reworked into a CDI {@link Interceptor} bound by the {@code @PremiumEndpoint} {@code @InterceptorBinding}; {@code @Around}/{@code ProceedingJoinPoint} became {@code @AroundInvoke}/{@link InvocationContext}. The Spring {@code ResponseStatusException(HttpStatus.FORBIDDEN, ...)} became a JAX-RS {@link WebApplicationException} wit...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ProprietaryWebMvcConfig.java:10` - TODO: Migration required - Spring MVC's WebMvcConfigurer / InterceptorRegistry has no Quarkus (JAX-RS / RESTEasy Reactive) equivalent, so this registration class cannot be ported directly.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ProprietaryWebMvcConfig.java:30` - TODO: Migration required - the interceptor registration below was removed: registry.addInterceptor(participantRateLimitInterceptor) .addPathPatterns("/api/v1/workflow/participant/**"); Re-implement as a JAX-RS ContainerRequestFilter bound to that path (see class javadoc).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:31` - Security configuration migrated from a Spring {@code @Configuration}/{@code @EnableWebSecurity} class to a Quarkus CDI bean. TODO: Migration required - This class was built entirely around the Spring Security {@code HttpSecurity} DSL and {@code SecurityFilterChain} beans, which have NO direct Quarkus equivalent. The HTTP security model must be re-expressed declaratively/imperatively: <ul> <li><b>HTTP path policies / authorization</b> (the {@code authorizeHttpRequests} rules: permit static res...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:95` - reusable, non-Spring helper logic (CORS values, X-Frame-Options decision, firewall char patterns, filter/repository factories) is retained as plain methods/producers below. TODO: Migration required - this bean was {@code @DependsOn("runningProOrHigher")} and {@code @Profile("!saas")}. The dependency ordering is approximated by injecting the {@code runningProOrHigher} flag; the {@code !saas} profile gate maps to a Quarkus build profile - use {@code @io.quarkus.arc.profile.UnlessBuildProfile("s...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:176` - Reusable CORS settings preserved from the original {@code corsConfigurationSource()} bean. TODO: Migration required - the Spring {@code CorsConfigurationSource}/ {@code UrlBasedCorsConfigurationSource} types are removed. Apply these values via {@code quarkus.http.cors.*} in {@code application.properties} (origins, methods, headers, exposed-headers, access-control-allow-credentials=true, access-control-max-age=PT1H) or a {@code ContainerResponseFilter}. The origin resolution from {@code applic...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:230` - Resolves the desired X-Frame-Options header value, preserving the original decision logic. TODO: Migration required - apply the returned value via a response filter or {@code quarkus.http.header} config (Spring's {@code HeadersConfigurer} is gone).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:253` - TODO: Migration required - samlFilterChain/filterChain/configureSecurity built the Spring SecurityFilterChain instances. Their behaviour is summarised in the class javadoc and must be reimplemented via Quarkus HTTP auth config + filters/IdentityProviders. The full original DSL is preserved in version control. No fabricated SecurityFilterChain is produced here.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:262` - Produces the IP rate-limiting filter (plain {@code jakarta.servlet.Filter}, not a Spring-specific type, so it remains a CDI producer). TODO: Migration required - registration/ordering must be handled by quarkus-undertow ({@code @WebFilter}) or a {@code ContainerRequestFilter}. This filter was already disabled in the original chain (limit is effectively a no-op at 1,000,000) pending conversion.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:289` - TODO: Migration required - JwtAuthenticationFilter is @ApplicationScoped with CDI field injection; CDI manages it directly. The @Produces factory was removed because constructing it here with explicit args is incompatible with how the bean is declared. Inject JwtAuthenticationFilter directly wherever it is needed.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java:328` - TODO: Migration required - SecurityContextHolder.clearContext() has no Quarkus equivalent; SecurityIdentity is request-scoped and not cleared imperatively. Cookie/ token invalidation is handled by the JWT cookie being dropped by the client/filter.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/EnterpriseEndpointFilter.java:21` - Spring's OncePerRequestFilter has no Quarkus equivalent; implementing jakarta.servlet.Filter directly. Registered via @WebFilter (quarkus-undertow). The single-execution-per-request guarantee OncePerRequestFilter provided is effectively given for top-level servlet filters here. TODO: Migration required - if this filter must run before/after other filters, ordering is not expressed by @WebFilter; configure quarkus.http.filter.* or a ServletExtension if order matters.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:47` - TODO: Migration required - registration/ordering. As a Spring OncePerRequestFilter this ran once per request at a Spring-defined position in the security filter chain. On Quarkus (quarkus-undertow) a jakarta.servlet.Filter needs explicit registration and ordering (e.g. a @WebFilter with urlPatterns, or a FilterRegistrationBean-style producer). Confirm this filter is registered ahead of the resource layer and that the once-per-request semantics are preserved (Undertow does not re-enter servlet...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:150` - TODO: Migration required - SecurityContextHolder has no Quarkus equivalent. This reads/writes the Spring thread-local security context. On Quarkus, the identity should come from SecurityIdentity (injected) and API-key auth should be handled by a custom IdentityProvider rather than imperatively setting the context.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:176` - TODO: Migration required - the previous ApiKeyAuthenticationToken extended Spring Security's AbstractAuthenticationToken. It is now a plain POJO that does not implement the security-compat Authentication contract, so it cannot be stored in the SecurityContext. Build a compat UsernamePasswordAuthenticationToken from the user's authorities to keep the API-key authentication intent; in Quarkus this should be a SecurityIdentity produced by a custom IdentityProvider for the API key.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:220` - TODO: Migration required - SecurityContextHolder/UsernamePasswordAuthenticationToken. Building a Spring authentication token and pushing it into the thread-local context must be replaced by producing a Quarkus SecurityIdentity (via IdentityProvider/ SecurityIdentityAugmentor) from the validated JWT claims. The user-loading logic (userDetailsService.loadUserByUsername) can be kept as a plain service call.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:243` - TODO: Migration required - Spring's WebAuthenticationDetailsSource (remote address + session id) has no Quarkus equivalent. Storing the request as the details object keeps the call compile-safe; in Quarkus this metadata is available from the RoutingContext / SecurityIdentity.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/ParticipantRateLimitInterceptor.java:71` - Do not trust X-Forwarded-For: it is user-controlled and trivially spoofed, which would allow an attacker to bypass this rate limiter by rotating fake IPs. Operators who deploy behind a trusted reverse proxy should configure Quarkus' quarkus.http.proxy.* (proxy-address-forwarding / trusted-proxies) at the framework level instead. TODO: Migration required - ContainerRequestContext does not expose the remote address. Inject quarkus' RoutingContext (io.vertx.ext.web.RoutingContext) or jakarta.ser...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:40` - TODO: Migration required - @Profile("!saas") had no direct annotation equivalent here. Gate this filter's activation on the "saas" build profile (e.g. via @io.quarkus.arc.profile.UnlessBuildProfile or a runtime check) and register it through Quarkus (quarkus-undertow @WebFilter or a jakarta.ws.rs.container.ContainerRequestFilter @Provider). Registration ordering relative to the other security filters (JwtAuthenticationFilter, *RateLimitingFilter) must be preserved.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:83` - Start each request clean so a pooled thread can't inherit a prior request's key label - but keep a label an upstream filter (JwtAuthenticationFilter) already set for a request it API-key-authenticated. TODO: Migration required - ApiKeyAuthenticationToken is a plain POJO here, so "already authenticated upstream" is the closest available test.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:90` - Spring's OncePerRequestFilter#shouldNotFilter behavior: skip the filter body for static resources, SPA routes and public API endpoints. TODO: Migration required - ensure the Quarkus filter registration does not run this filter more than once per request (the OncePerRequestFilter guarantee).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:319` - Was Spring's OncePerRequestFilter#shouldNotFilter; now called explicitly at the top of doFilter. TODO: Migration required - if registered as a ContainerRequestFilter instead of a servlet Filter, fold this skip logic into the request filter using UriInfo.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserBasedRateLimitingFilter.java:32` - Servlet filter retained (quarkus-undertow). Spring's OncePerRequestFilter replaced by a plain jakarta.servlet.Filter registered as a CDI bean via @WebFilter so it covers all requests; the rate-limiting logic operates on the raw HttpServletRequest/HttpServletResponse which a JAX-RS ContainerRequestFilter does not expose as conveniently. TODO: Migration required - Spring's @Profile("!saas") gated this filter so it was NOT registered in the "saas" profile. Quarkus has no per-profile bean exclusi...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserBasedRateLimitingFilter.java:48` - TODO: Migration required - SecurityContextHolder replaced by injected SecurityIdentity. SecurityIdentity is request-scoped and is populated by Quarkus security extensions (quarkus-elytron-security / quarkus-oidc / etc.) once authentication is migrated. Until then it will be anonymous and getRoleFromIdentity will fall through to the IllegalStateException.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/ApiKeyAuthenticationToken.java:6` - TODO: Migration required - this class extended Spring Security's org.springframework.security.authentication.AbstractAuthenticationToken (which implements org.springframework.security.core.Authentication). Quarkus has no equivalent token type; the runtime principal model is io.quarkus.security.identity.SecurityIdentity, typically built via a custom IdentityProvider / SecurityIdentityAugmentor for the API-key auth path. This class has been reduced to a plain POJO that preserves the principal/c...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/TauriAuthorizationRequestResolver.java:6` - TODO: Migration required - this class implemented Spring Security's org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver SPI, wrapping DefaultOAuth2AuthorizationRequestResolver (built from a ClientRegistrationRepository) to inject a custom "tauri:" state value before the authorization request is sent to the OAuth2 provider. quarkus-oidc has no equivalent pluggable AuthorizationRequestResolver SPI. The Spring glue (OAuth2AuthorizationRequestResolver, DefaultOAuth2A...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:167` - Resolve through the shared service (multi-key table, then the legacy per-user column). The key runs as its owner with the owner's authorities. TODO: Migration required - emits a Spring-shaped Authentication consumed by the auth filters; replace with a SecurityIdentity construction once the filter layer is ported.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionPersistentRegistry.java:25` - TODO: Migration required - this class implements the SessionRegistry compatibility shim (stirling.software.common.security.SessionRegistry) and exposes SessionInformation, UserDetails and OAuth2User from the same compat package. Quarkus has no equivalent session-registry abstraction. These shim types are kept ONLY because un-migrated collaborators (UserAuthenticationFilter, UserService, SessionRegistryConfig) still consume this interface and its return types. Once those collaborators are migr...
- `app/proprietary/src/main/java/stirling/software/proprietary/web/AuditWebFilter.java:25` - Servlet filter retained (quarkus-undertow). Spring's OncePerRequestFilter replaced by a plain jakarta.servlet.Filter registered as a CDI bean via @WebFilter so it covers all requests. TODO: Migration required - Spring's @Order(Ordered.HIGHEST_PRECEDENCE + 10) ordering has no direct @WebFilter equivalent; if this filter must run before other servlet filters, configure ordering explicitly (e.g. via a FilterRegistrationBean equivalent / quarkus.http.filter.* in application.properties).
- `app/proprietary/src/main/java/stirling/software/proprietary/web/CorrelationIdFilter.java:22` - TODO: Migration required - quarkus-undertow provides jakarta.servlet support. Register this filter and its URL mapping/ordering via a @WebFilter annotation or a ServletExtension if order matters (Spring auto-registered @Component filters; Quarkus does not).
- `app/saas/build.gradle:14` - spring-boot-starter-webmvc -> quarkus-rest (inherited api-scoped from :common). REMOVED: spring-boot-starter-aspectj - no AspectJ in Quarkus; use quarkus-arc CDI interceptors. TODO: Migration required - rewrite any @Aspect advice (e.g. CreditSuccessAdvice) as CDI interceptors.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:75` - TODO: Migration required - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:109` - TODO: Migration required - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:141` - TODO: Migration required - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:189` - TODO: Migration required - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:244` - TODO: Migration required - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:268` - TODO: Migration required - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:344` - TODO: Migration required - @PreAuthorize("@teamSecurity.isTeamMember(#teamId)") complex SpEL; enforce team-membership check programmatically or via a JAX-RS filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:363` - TODO: Migration required - @PreAuthorize("@teamSecurity.isTeamLeader(#teamId)") complex SpEL; enforce team-leader check programmatically or via a JAX-RS filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:382` - TODO: Migration required - @PreAuthorize("@teamSecurity.isTeamLeader(#teamId)") complex SpEL; enforce team-leader check programmatically or via a JAX-RS filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:436` - TODO: Migration required - @PreAuthorize("@teamSecurity.isTeamMember(#teamId)") complex SpEL; enforce team-membership check programmatically or via a JAX-RS filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:485` - TODO: Migration required - @PreAuthorize("@teamSecurity.isTeamLeader(#teamId)") complex SpEL; enforce team-leader check programmatically or via a JAX-RS filter.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:727` - TODO: Migration required - @PreAuthorize("@teamSecurity.isTeamMember(#teamId) or hasRole('ADMIN')") complex SpEL; enforce team-membership-or-admin check programmatically or via a JAX-RS filter.
- `app/saas/src/main/java/stirling/software/saas/controller/UserRoleWebhookController.java:195` - TODO: Migration required - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter. TODO: Migration required - inject Principal via @jakarta.ws.rs.core.Context SecurityContext (JAX-RS does not bind a bare java.security.Principal parameter like Spring MVC).
- `app/saas/src/main/java/stirling/software/saas/payg/api/PaygWalletController.java:72` - cap is enforced application-side via the entitlement guard) and invalidates the team's snapshot cache. Only leaders may call this; the team is derived from the caller, so we authorise inside the method — the team id never appears on the path or query string. TODO: Migration required - was a Spring {@code @RestController} with method-injected {@code Authentication} and {@code @PreAuthorize("isAuthenticated()")}. Now JAX-RS: auth comes from the {@link SecurityContextHolder} thread-local shim (p...
- `app/saas/src/main/java/stirling/software/saas/payg/cap/AiToolRoutes.java:29` - TODO: Migration required - literal value of the former Spring constant HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE. Replace with the JAX-RS route template (UriInfo / ResourceInfo) once the interceptor is converted to a @Provider filter.
- `app/saas/src/main/java/stirling/software/saas/payg/charge/JobInput.java:30` - TODO: Migration required - Part/MultipartFile bridge. The ingress interceptor (PaygChargeInterceptor) is now servlet-native and constructs inputs from jakarta.servlet.http.Part rather than Spring's MultipartFile. The downstream classifier still consumes the stirling.software.common.model.MultipartFile abstraction (size + content-type + input stream). This constructor adapts a Part into that abstraction so both the untouched interceptor and the classifier compile/run. Longer term, JobInput sho...
- `app/saas/src/main/java/stirling/software/saas/payg/entitlement/EntitlementGuard.java:64` - pipeline must never block a customer because the guard tripped on a transient DB error. TODO: Migration required - was a Spring {@code @Component} implementing {@code HandlerInterceptor}. Convert to a JAX-RS {@code @Provider} ContainerRequestFilter (priority {@code PaygWebMvcConfig.ENTITLEMENT_GUARD_ORDER}). Handler-annotation introspection now uses a reflective {@link Method} fallback; HTTP status/header/media-type constants are inlined literals.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:69` - and counted on {@code payg.filter.errors}. The customer's tool call always proceeds. TODO: Migration required - was a Spring {@code @Component} ({@code @Profile("saas")}) implementing {@code AsyncHandlerInterceptor}. Convert to a JAX-RS {@code @Provider} request/response filter pair. Handler-annotation introspection now uses a reflective {@link Method} fallback (see {@link #resolveResourceMethod}); multipart access uses the servlet-native {@link Part} API ({@code request.getParts()}); the bes...
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:91` - TODO: Migration required - literal value of the former Spring constant {@code HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE}. Replace with the JAX-RS route template obtained from {@code @Context UriInfo} / {@code ResourceInfo} during the filter conversion.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:181` - TODO: Migration required - was @Override AsyncHandlerInterceptor#preHandle(request, response, handler). Convert to a JAX-RS ContainerRequestFilter.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:242` - TODO: Migration required - was `request instanceof MultipartHttpServletRequest mreq` + mreq.getMultiFileMap(). Now uses servlet-native request.getParts(). A non-multipart request yields no file parts and short-circuits, preserving the original behavior.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:332` - TODO: Migration required - the {@link JobInput} record's first component is still Spring's {@code MultipartFile} (owned by another module). This interceptor now sources inputs from the servlet {@link Part} API. Once {@code JobInput} is migrated to carry a {@link Part} (or a neutral size+content-type holder), construct it directly here: {@code return new JobInput(part, path);}. Kept as a single adaptation seam so the rest of the charge flow is untouched.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:343` - TODO: Migration required - was @Override AsyncHandlerInterceptor#afterCompletion(request, response, handler, Exception). Convert to a JAX-RS ContainerResponseFilter.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:488` - TODO: Migration required - was @Override AsyncHandlerInterceptor#afterConcurrentHandlingStarted. JAX-RS handles async dispatch differently; no direct equivalent required.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygFilterProperties.java:24` - TODO: Migration required - @ConfigurationProperties(prefix="payg.filter"); bind via @ConfigProperty or @ConfigMapping
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java:33` - TODO: Migration required - this was a Spring {@code OncePerRequestFilter} ({@code @Component @Profile("saas")}). It must be re-registered as a {@code jakarta.servlet.Filter} (or a JAX-RS {@code @jakarta.ws.rs.ext.Provider} ContainerResponse filter pair) and ordered ahead of the PAYG interceptor so the response wrapper is available in afterCompletion. The Spring base class provided once-per-request dispatch and the {@code doFilterInternal} hook; that hook's servlet signature is retained below ...
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java:61` - TODO: Migration required - was @Override of Spring OncePerRequestFilter#doFilterInternal. Retains the servlet signature; invoke from the filter registration's doFilter once converted.
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygWebMvcConfig.java:13` - Holds the PAYG hot-path ordering constants. Under Spring MVC these registered {@link PaygChargeInterceptor} and the entitlement guard as ordered interceptors; under Quarkus the interceptor/guard are JAX-RS filters that self-order via {@code @Priority}. The order constants remain the single source of truth for that relative ordering. TODO: Migration required - the Spring {@code WebMvcConfigurer#addInterceptors} registration was removed. Re-express it as JAX-RS {@code @Provider} ContainerReques...
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:55` - Stateless JWT authentication filter for the saas profile. TODO: Migration required - this was a Spring {@code OncePerRequestFilter}. It must be re-registered as a JAX-RS {@code @jakarta.ws.rs.container.ContainerRequestFilter} with {@code @jakarta.ws.rs.ext.Provider} (or a {@code jakarta.servlet.Filter}) and ordered before the Quarkus OIDC/auth processing. The {@code doFilterInternal}/{@code shouldNotFilter} servlet signatures are retained here; the request/response handling and entry-point er...
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:72` - TODO: Migration required - placeholder for Spring's {@code org.springframework.security.oauth2.jwt.JwtDecoder}. Replace with Quarkus OIDC token parsing that yields a verified {@link JsonWebToken} (or throws on invalid token).
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:88` - TODO: Migration required - the Spring AuthenticationEntryPoint (BearerTokenAuthenticationEntryPoint) that wrote the 401 challenge has no Quarkus equivalent here. When converting to a JAX-RS @Provider filter, emit the 401 / WWW-Authenticate response directly (or delegate to Quarkus OIDC) in place of authenticationEntryPoint.commence(...).
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:108` - TODO: Migration required - this retains the original OncePerRequestFilter.doFilterInternal behavior. Wire it into a JAX-RS ContainerRequestFilter / servlet Filter. The error branch previously called authenticationEntryPoint.commence(request, response, e); emit the 401 response directly during that conversion.
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:143` - TODO: Migration required - was authenticationEntryPoint.commence(request, response, e) (Spring BearerTokenAuthenticationEntryPoint). Emit the 401 challenge response here when converting to a JAX-RS @Provider filter.
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:219` - TODO: Migration required - previously caught Spring's JwtException and rethrew InvalidBearerTokenException("Invalid JWT", e). Adjust to the exception type thrown by the Quarkus OIDC token parser.
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:313` - TODO: Migration required - was Spring's DataIntegrityViolationException (email-collision race). jakarta.persistence.PersistenceException is broader; narrow to the Hibernate/JPA constraint-violation type once the persistence layer is finalized.
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:409` - Concurrent creation; fall through, the row exists. TODO: Migration required - was Spring's DataIntegrityViolationException. Narrow to the Hibernate/JPA constraint-violation type once the persistence layer is finalized.
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:424` - Parallel filter won the race; fetch the winning row. TODO: Migration required - was Spring's DataIntegrityViolationException. Narrow to the Hibernate/JPA constraint-violation type once the persistence layer is finalized.
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:457` - TODO: Migration required - ApiKeyAuthenticationToken is a plain POJO that does not implement the Authentication shim. Wrap the principal/credentials/authorities in a UsernamePasswordAuthenticationToken (which does) so it can be set on the SecurityContext. Re-wire to a Quarkus SecurityIdentity when the API-key auth path is migrated.
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:483` - --------------------------------------------------------------------------------------------- TODO: Migration required - claim accessor adapters. Spring's Jwt exposed typed claim getters (getClaimAsString/getClaimAsStringList/getClaimAsInstant/getClaimAsBoolean). MicroProfile JsonWebToken only exposes a generic getClaim(name); these helpers reproduce the original typed semantics so the validation/user-creation logic is preserved unchanged. -----------------------------------------------------...
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:38` - Stateless Supabase-JWT security chain. TODO: Migration required - this class was a Spring {@code @Configuration} with {@code @EnableWebSecurity}, {@code @EnableMethodSecurity}, {@code @Profile("saas")} and {@code @Order(1)}. The {@code SecurityFilterChain} bean (CSRF/CORS/session/oauth2ResourceServer wiring) has no Quarkus equivalent and must be re-expressed declaratively via {@code quarkus.http.auth.*} config plus Quarkus OIDC/SmallRye-JWT. The {@code SecurityFilterChain} bean method has bee...
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:72` - TODO: Migration required - the original @Bean SecurityFilterChain saasSecurityFilterChain(...) configured CSRF-disabled, CORS, STATELESS sessions, permitAll matchers for OPTIONS/actuator-health/config/static/public-auth/frontend routes, anyRequest().authenticated(), registered SupabaseAuthenticationFilter before BearerTokenAuthenticationFilter, set a BearerTokenAuthenticationEntryPoint + BearerTokenAccessDeniedHandler, and wired oauth2ResourceServer().jwt() with this JwtDecoder and SupabaseSe...
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:168` - TODO: Migration required - original @Bean CorsConfigurationSource configured CORS for the Spring SecurityFilterChain (allowed origins/methods/headers, exposed header WWW-Authenticate, allowCredentials=true, maxAge=3600). Re-express via quarkus.http.cors.* properties. The origin-resolution logic (operator override vs. defaults, the Tauri desktop origins, and the wildcard warning) is retained below as a helper for that translation.
</details>
<details><summary><b>Spring Security -> quarkus-oidc / SecurityIdentity</b> (91)</summary>
- `app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java:15` - Spring Boot test framework not available in Quarkus
- `app/proprietary/build.gradle:44` - ---- SAML2: no native Quarkus extension. Rehosted on OpenSAML 5 (already pinned via openSamlVersion) following the dnulnets/quarkus-saml example. spring-security-saml2- service-provider and spring-security-core are removed; the SAML wiring is reimplemented on a Jakarta servlet + OpenSAML 5 (quarkus-undertow provides the servlet runtime). TODO: Migration required - reimplement Saml2Configuration / CustomSaml2* on OpenSAML 5. ----
- `app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java:54` - TODO: Migration required - this previously wrapped the executor in Spring Security's DelegatingSecurityContextExecutor to propagate the SecurityContext onto background threads. Quarkus has no direct equivalent; the SecurityIdentity must be captured on the caller thread and re-established on the worker thread (e.g. via a captured io.quarkus.security.identity.SecurityIdentity or org.eclipse.microprofile.context.ThreadContext from MicroProfile Context Propagation). For now only MDC context is pr...
- `app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java:447` - TODO: Migration required - Spring distinguished UserDetails / OAuth2User / CustomSaml2AuthenticatedPrincipal off authentication.getPrincipal() to set the oAuth2Login / saml2Login flags. Under Quarkus the auth mechanism is exposed via SecurityIdentity attributes (e.g. quarkus-oidc IdToken / SAML augmentor). Until OAuth2/ SAML are wired to quarkus-oidc, only the username is resolved and the login-type flags default to false.
- `app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java:36` - Controller for managing user signatures in proprietary/authenticated mode only. Requires user authentication and enforces per-user storage limits. TODO: Migration required - the original endpoints were guarded by Spring Security SpEL expressions ({@code @PreAuthorize("isAuthenticated() && !hasAuthority('ROLE_DEMO_USER')")} and {@code @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")}). These are not simple role checks, so they cannot be expressed with {@code @RolesAllowed}. Authentication shou...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/McpServerController.java:229` - TODO: Migration required - the Spring code derived scopes from GrantedAuthority values prefixed with "SCOPE_". Quarkus SecurityIdentity.getRoles() typically already carries the bare role/scope names (quarkus-oidc maps OIDC scopes to roles without the SCOPE_ prefix). Confirm the configured quarkus.oidc role/scope mapping; if scopes arrive as a "scope" claim, read them via securityIdentity.getAttribute("scope")/getClaims() instead. For now we accept both the bare role and any "SCOPE_"-prefixed ...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:56` - TODO: Migration required - replace Spring exception type checks below (DisabledException, LockedException, BadCredentialsException, UsernameNotFoundException, InternalAuthenticationServiceException) with the Quarkus authentication-failure type(s), and replace each getRedirectStrategy().sendRedirect(request, response, "...") call with a Quarkus redirect (e.g. response.sendRedirect(...) or building a 302 jakarta.ws.rs.core.Response from the auth mechanism).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:66` - TODO: Migration required - sendRedirect("/logout?userIsDisabled=true")
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:74` - TODO: Migration required - sendRedirect("/login?error=locked")
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:88` - TODO: Migration required - sendRedirect("/login?error=locked")
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:93` - TODO: Migration required - sendRedirect("/login?error=badCredentials")
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:98` - TODO: Migration required - sendRedirect("/login?error=oauth2AuthenticationError")
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:102` - TODO: Migration required - default failure handling previously delegated to SimpleUrlAuthenticationFailureHandler.onAuthenticationFailure (redirect to the configured failure URL).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:107` - TODO: Migration required - these predicates stand in for Spring Security's exception type hierarchy and must be rewired to the Quarkus authentication-failure type(s) once the auth mechanism is chosen.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:58` - TODO: Migration required - signature changed from Spring's onAuthenticationSuccess(HttpServletRequest, HttpServletResponse, org.springframework.security.core.Authentication). The Spring Authentication parameter has been dropped here; JwtServiceInterface#generateToken(Authentication, ...) still requires it (JwtServiceInterface is a separate file that must be migrated to accept a Quarkus SecurityIdentity / principal). For now the username is read from the request parameter as before; wire the a...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:78` - TODO: Migration required - JwtServiceInterface#generateToken expected a Spring Authentication. Pass the migrated Quarkus identity once JwtServiceInterface is ported; generating the token by username for now.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:112` - TODO: Migration required - placeholder for reading the redirect URL off whatever object the migrated request cache stores. The Spring SavedRequest#getRedirectUrl() is gone.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:131` - TODO: Migration required - the following Spring-Security collaborators were injected as @Autowired(required=false) optional beans and consumed only inside the removed HttpSecurity DSL (GrantedAuthoritiesMapper, RelyingPartyRegistrationRepository, OpenSaml5AuthenticationRequestResolver, ClientRegistrationRepository, PasswordEncoder). They are dropped here because their types are Spring-Security-only; reintroduce equivalents (quarkus-oidc client config, OpenSAML 5 SP wiring, a CDI password hash...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java:280` - TODO: Migration required - was SecurityContextHolder.getContext().getAuthentication(). Quarkus SecurityIdentity has no Spring UserDetails principal; loading the full User here requires a SecurityIdentityAugmentor that attaches the User (or re-loading via userDetailsService by name). Until then we re-load the user from the identity name.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/identity/UserSecurityIdentityAugmentor.java:25` - Attaches the {@link User} entity as the {@link SecurityIdentity} principal for any authenticated request. Spring exposed the {@code User} directly via {@code Authentication#getPrincipal()} (it implemented {@code UserDetails}), so a lot of the code base does {@code principal instanceof User} (folders, file storage, sessions, audit, UserController). This augmentor restores that for the Quarkus auth paths (JWT Bearer, X-API-KEY, and later OIDC/SAML): it re-loads the user by name and rebuilds the...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/Authority.java:20` - TODO: Migration required - this entity previously implemented Spring Security's org.springframework.security.core.GrantedAuthority. That interface only required String getAuthority(), which the Lombok @Getter on the 'authority' field still provides. Quarkus uses its own role model (SecurityIdentity roles); when wiring the IdentityProvider that loads users, map this 'authority' value into the granted roles.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/User.java:40` - TODO: Migration required - this entity previously implemented org.springframework.security.core.userdetails.UserDetails. Quarkus has no UserDetails contract; the user-loading/principal adaptation must be rehosted in a Quarkus IdentityProvider (or SecurityIdentityAugmentor) that builds a SecurityIdentity from this entity. The Lombok getters still expose getUsername()/getPassword()/getAuthorities()/ isEnabled() so that adapter can read them directly. isEnabled() override below is retained as pl...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/exception/AuthenticationFailureException.java:3` - TODO: Migration required - originally extended org.springframework.security.core.AuthenticationException (Spring Security). Quarkus has no direct equivalent base type; extend RuntimeException so this remains a usable application exception. If integrated with quarkus-security, consider mapping to io.quarkus.security.AuthenticationFailedException.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:33` - TODO: Migration required - this class extended Spring Security's SavedRequestAwareAuthenticationSuccessHandler, which has no Quarkus equivalent. Under quarkus-oidc there is no AuthenticationSuccessHandler concept; the post-login OAuth2 success flow must be rehosted, e.g. via a SecurityIdentityAugmentor plus a JAX-RS callback resource (or a jakarta.servlet endpoint) that performs the redirect/JWT-issuance below. The Spring Authentication/OAuth2User/OAuth2AuthenticationToken/SavedRequest types ...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:54` - TODO: Migration required - the original signature took a Spring Security org.springframework.security.core.Authentication. Under quarkus-oidc this should receive an io.quarkus.security.identity.SecurityIdentity (or the OIDC IdToken/UserInfo). The "authentication" parameter is now typed as Object so the body still compiles; replace it with the real quarkus-oidc principal type and re-implement principal extraction below when wiring the success flow.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:67` - TODO: Migration required - principal extraction relied on Spring Security OAuth2User / UserDetails. Derive the username from the quarkus-oidc principal (SecurityIdentity / IdToken claims) instead.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:100` - TODO: Migration required - SavedRequest / "SPRING_SECURITY_SAVED_REQUEST" is a Spring Security web construct. Under quarkus-oidc the original target URL is preserved via the OIDC state/restore-path mechanism (quarkus.oidc.authentication.restore-path-after-redirect) rather than a session attribute. Re-implement saved-request resolution accordingly; the session attribute read below is left as a placeholder and will currently be null.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:112` - TODO: Migration required - originally delegated to SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccess to redirect to the saved request. Reimplement the redirect to the saved/original destination here once the quarkus-oidc saved-request mechanism is in place.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:122` - TODO: Migration required - originally threw Spring Security's org.springframework.security.authentication.LockedException. Replace with the exception type the quarkus-oidc success flow expects (or a redirect to a locked page); throwing a plain IllegalStateException here as a placeholder.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:130` - TODO: Migration required - originally used Spring's RedirectStrategy via getRedirectStrategy().sendRedirect(...). Using the servlet response directly.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:154` - TODO: Migration required - SSO provider/claims extraction relied on Spring Security's OAuth2User attributes and OAuth2AuthenticationToken. Re-derive the OIDC "sub" claim and the provider registration id from the quarkus-oidc principal.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:189` - Web: Use default expiry TODO: Migration required - JwtServiceInterface.generateToken(Authentication, claims) takes a Spring Security Authentication. Until JwtServiceInterface is migrated, issue the token by username (same identity) to avoid the Spring dependency here.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:214` - TODO: Migration required - placeholder for principal -> username extraction. Originally used Spring Security OAuth2User.getName() / UserDetails.getUsername(). Implement against the quarkus-oidc principal (SecurityIdentity.getPrincipal().getName() / IdToken claims).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:219` - "TODO: Migration required - extract username from the quarkus-oidc principal");
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:222` - TODO: Migration required - placeholder for the OIDC "sub" claim. Originally oAuth2User.getAttribute("sub"). Read it from the quarkus-oidc IdToken/UserInfo.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:226` - "TODO: Migration required - extract the 'sub' claim from the quarkus-oidc principal");
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:229` - TODO: Migration required - placeholder for the saved-request redirect URL. Originally SavedRequest.getRedirectUrl().
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:233` - "TODO: Migration required - resolve the saved-request redirect URL under quarkus-oidc");
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:236` - TODO: Migration required - placeholder for delegating to the saved-request redirect. Originally SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccess(...).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:242` - "TODO: Migration required - redirect to the saved/original destination under quarkus-oidc");
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:252` - TODO: Migration required - originally cast to Spring Security's OAuth2AuthenticationToken and called getAuthorizedClientRegistrationId(). Derive the OIDC provider/tenant id from the quarkus-oidc principal instead.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:388` - TODO: Migration required - originally built the Set-Cookie value with Spring's org.springframework.http.ResponseCookie. Replaced with a manually built RFC 6265 Set-Cookie string to drop the Spring HTTP dependency. Consider switching to jakarta.servlet.http.Cookie / response.addCookie once SameSite handling is confirmed.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:27` - TODO: Migration required - OAuth2 client/login is a Spring Security feature (org.springframework.security.oauth2.client.*) with NO direct Quarkus equivalent. In Quarkus the OIDC/OAuth2 client is configured declaratively via quarkus-oidc (quarkus.oidc.* and named tenants quarkus.oidc.<tenant>.* in application.properties), not by programmatically building a ClientRegistrationRepository. This class previously @Produces'd a ClientRegistrationRepository and a GrantedAuthoritiesMapper. Those produc...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:50` - TODO: Migration required - @Lazy has no Quarkus equivalent; CDI proxies break the original lazy cycle. UserService is injected eagerly. If a genuine lazy/circular dependency exists, switch to jakarta.enterprise.inject.Instance<UserService> and resolve at call time.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:70` - Resolves the set of configured OAuth2 providers from ApplicationProperties and validates each one. The original implementation built a Spring Security ClientRegistrationRepository from these providers. TODO: Migration required - the return type was org.springframework.security.oauth2.client.registration.ClientRegistrationRepository, produced via Spring @Bean. quarkus-oidc does not consume a ClientRegistrationRepository; instead each validated Provider below must be emitted as a named OIDC ten...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:116` - TODO: Migration required - the original built a ClientRegistration via ClientRegistrations.fromIssuerLocation(issuer) (OIDC discovery). Under quarkus-oidc this maps to quarkus.oidc.<name>.auth-server-url=<issuer> with discovery enabled, plus client-id/credentials.secret/authentication.scopes/token-state username attribute.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:139` - TODO: Migration required - the original built a ClientRegistration with explicit authorizationUri/tokenUri/userInfoUri + redirectUri(REDIRECT_URI_PATH + name) + AUTHORIZATION_CODE grant. Under quarkus-oidc this maps to a named tenant quarkus.oidc.google.* (authorization-path/token-path/user-info-path or auth-server-url, authentication.redirect-path, application-type=web-app). Google's endpoints come from the GoogleProvider getters below.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:175` - TODO: Migration required - the original built a ClientRegistration with explicit authorizationUri/tokenUri/userInfoUri + redirectUri(REDIRECT_URI_PATH + name) + AUTHORIZATION_CODE grant. Map to quarkus.oidc.github.* tenant config (GitHub is a plain OAuth2, not OIDC, provider - quarkus-oidc may require provider=github or explicit *-path settings).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:220` - TODO: Migration required - the original built a ClientRegistration via ClientRegistrations.fromIssuerLocation(issuer) (OIDC discovery) with redirectUri(REDIRECT_URI_PATH + name) + AUTHORIZATION_CODE grant. Map to a named tenant quarkus.oidc.<name>.auth-server-url=<issuer> (discovery on), client-id/credentials.secret/authentication.scopes, authentication.redirect-path.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:241` - TODO: Migration required - this was a Spring Security
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CertificateUtils.java:19` - TODO: Migration required - the original @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") gated this class on a runtime property. This is a utility holding only static methods (not a CDI bean), so the annotation was a no-op for instantiation and is dropped. Callers must enforce the security.saml2.enabled runtime toggle (e.g. via a runtime guard at the SAML SP entry point); see the SAML2 migration notes.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2AuthenticatedPrincipal.java:7` - TODO: Migration required - this record implemented Spring Security's org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal. There is NO Quarkus SAML extension; the SAML SP must be rehosted on a Jakarta @WebServlet using OpenSAML 5 (dnulnets/quarkus-saml pattern). The OpenSAML-derived principal data (name, attributes, nameId, sessionIndexes) is preserved below as a plain data carrier; re-wire it into the replacement SAML authentication flow / SecurityId...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:23` - TODO: Migration required - there is NO Quarkus SAML extension. This class previously implemented Spring Security's org.springframework.security.core.convert.converter.Converter< OpenSaml5AuthenticationProvider.ResponseToken, Saml2Authentication> to plug into Spring's SAML2 OpenSaml5AuthenticationProvider pipeline. The OpenSAML 5 (org.opensaml.*) assertion/attribute extraction logic below is preserved unchanged. The Spring SAML2 glue has been removed: - org.springframework.security.saml2.provi...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:83` - TODO: Migration required - signature changed from convert(OpenSaml5AuthenticationProvider.ResponseToken) returning Saml2Authentication. Re-wire the input to the OpenSAML 5 Assertion obtained from the rehosted SAML SP and the output to a Quarkus SecurityIdentity. The OpenSAML attribute/identifier/session-index extraction logic below is the reusable part and is preserved. The returned CustomSaml2AuthenticatedPrincipal plus the resolved role (ROLE_USER or the user's role) carry the data the new ...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:115` - TODO: Migration required - resolved authority was previously wrapped in a Spring SimpleGrantedAuthority("ROLE_USER" / userService.findRole(user)). Map this role String onto a Quarkus SecurityIdentity role when wiring the SAML SP / SecurityIdentityAugmentor.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:13` - TODO: Migration required - this class implemented Spring Security's org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository over Saml2PostAuthenticationRequest / RelyingPartyRegistration(Repository). There is NO Quarkus SAML extension, so the Spring Security SAML glue (interface, Saml2PostAuthenticationRequest, RelyingPartyRegistration[Repository]) has been removed. The SAML SP must be rehosted on a Jakarta @WebServlet using OpenSAML 5 (see the dnulnets/qu...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:39` - TODO: Migration required - original signature was saveAuthenticationRequest(Saml2PostAuthenticationRequest authRequest, HttpServletRequest, HttpServletResponse). Pass the OpenSAML-derived claims + relayState once the SP is rehosted.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:66` - TODO: Migration required - original returned Saml2PostAuthenticationRequest. Map the returned claims back to the OpenSAML AuthnRequest model once the SP is rehosted.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:80` - TODO: Migration required - original returned Saml2PostAuthenticationRequest.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:112` - TODO: Migration required - original signature was serializeSamlRequest(Saml2PostAuthenticationRequest authRequest). Build this claims map from the OpenSAML AuthnRequest fields (id, relyingPartyRegistrationId / SP entity id, authenticationRequestUri / destination, samlRequest, relayState) once the SP is rehosted.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:133` - TODO: Migration required - original returned Saml2PostAuthenticationRequest rebuilt via Saml2PostAuthenticationRequest.withRelyingPartyRegistration(...). Resolve the RelyingPartyRegistration equivalent (SP metadata) and rebuild the OpenSAML AuthnRequest from these claims once the SP is rehosted. For now the raw claims map is returned unchanged.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:19` - TODO: Migration required - there is NO Quarkus SAML extension. The original class was a Spring @Configuration that exposed two @Bean factory methods producing Spring Security SAML2 types (org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository and ...web.authentication.OpenSaml5AuthenticationRequestResolver). Those builder/glue types have no Quarkus equivalent, so the Spring Security SAML2 imports and @Configuration/@Bean wiring have been removed. T...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:41` - TODO: Migration required - originally a @Bean returning Spring Security's RelyingPartyRegistrationRepository built via RelyingPartyRegistration.withRegistrationId(...) (InMemoryRelyingPartyRegistrationRepository, Saml2X509Credential, Saml2MessageBinding). Those Spring Security SAML2 builder types are unavailable in Quarkus. The credential loading (CertificateUtils via the common Resource shim) and the entityId / ACS / SLO location strings are kept verbatim so the OpenSAML-5-based SP rehost ca...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:75` - TODO: Migration required - was Saml2X509Credential.verification(idpCert). Re-create the IdP verification credential from idpCert using OpenSAML 5 (BasicX509Credential).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:98` - TODO: Migration required - was new Saml2X509Credential(privateKey, cert, Saml2X509CredentialType.SIGNING). Build the SP signing credential from the key/cert below using OpenSAML 5 (BasicX509Credential) instead of Spring Security's Saml2X509Credential.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:125` - TODO: Migration required - the following Spring Security RelyingPartyRegistration was built here and stored in an InMemoryRelyingPartyRegistrationRepository. Re-implement against the OpenSAML-5-based SP using entityId / acsLocation / sloResponseLocation, the IdP issuer (samlConf.getIdpIssuer()), SSO/SLO bindings (POST) and locations (samlConf.getIdpSingleLoginUrl() / samlConf.getIdpSingleLogoutUrl()), authnRequestsSigned and wantAuthnRequestsSigned both true, and the signing/verification cred...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:142` - TODO: Migration required - originally a @Bean returning Spring Security's OpenSaml5AuthenticationRequestResolver, configured with a RelayState resolver and an AuthnRequest customizer. That resolver type is Spring-Security-specific and has no Quarkus equivalent. The RelayState logic (Tauri detection -> TauriSamlUtils.buildRelayState(nonce)) and the AuthnRequest customization (unique ARQ id + logging) are PRESERVED below as helper methods so the OpenSAML-5-based SP rehost can invoke them when b...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/AppUpdateAuthService.java:24` - TODO: Migration required - SecurityIdentity is request-scoped; injecting it into an @ApplicationScoped bean relies on Quarkus' client proxy resolving the current request's identity. Verify this resolves correctly when invoked outside an active HTTP request (e.g. scheduled/background contexts), where the identity may be anonymous/null.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:18` - TODO: Migration required - quarkus-oidc has no equivalent of Spring's OAuth2UserService<OidcUserRequest, OidcUser> / OidcUserService delegate. Under quarkus-oidc the OIDC flow is handled by the extension (quarkus.oidc.* config); per-login user mapping and the "useAsUsername" claim selection should be re-implemented in a io.quarkus.security.identity.SecurityIdentityAugmentor (inject the @io.quarkus.oidc.IdToken JsonWebToken / OidcSession), and the blocked-account / hasPassword checks below sho...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:58` - Resolves and validates the local user for an OIDC login. TODO: Migration required - this method previously implemented Spring's {@code OAuth2UserService<OidcUserRequest, OidcUser>.loadUser}. Under quarkus-oidc there is no user-request object handed to application code; instead call this logic from a {@code SecurityIdentityAugmentor} once quarkus-oidc has produced the {@code SecurityIdentity}. Provide the registration/tenant id, the merged claim map and the ID-token claim map from the augmento...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:114` - TODO: Migration required - was org.springframework.security.authentication .LockedException; surface this as io.quarkus.security.AuthenticationFailedException (or a custom locked-account exception) from the SecurityIdentityAugmentor.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:147` - TODO: Migration required - was wrapped as org.springframework.security.oauth2.core.OAuth2AuthenticationException(OAuth2Error); rethrow as io.quarkus.security.AuthenticationFailedException from the augmentor.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:163` - TODO: Migration required - was OAuth2AuthenticationException("Unexpected error during authentication"); rethrow as io.quarkus.security.AuthenticationFailedException.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomUserDetailsService.java:16` - TODO: Migration required - this class implemented org.springframework.security.core.userdetails.UserDetailsService and returned a org.springframework.security.core.userdetails.UserDetails. Quarkus has no UserDetailsService contract; the user-loading logic below should be invoked from a Quarkus IdentityProvider (or SecurityIdentityAugmentor) that turns the returned User into a SecurityIdentity. The method is retained as a plain service returning the User entity. Former Spring exceptions are ma...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtServiceInterface.java:17` - TODO: Migration required - the implementation must derive the username/claims from SecurityIdentity (getPrincipal()/getRoles()) instead of the former Spring Authentication.getName()/getAuthorities().
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:684` - TODO: Migration required - SessionPersistentRegistry still exposes Spring Security types (SessionInformation, UserDetails, OAuth2User). Once that collaborator is ported to a Quarkus session store, drop these Spring Security imports and adjust the principal type checks accordingly.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/session/SessionRegistryConfig.java:8` - TODO: Migration required - SessionRegistryImpl is a Spring Security type (org.springframework.security.core.session.SessionRegistryImpl) with no Quarkus equivalent. Concurrent-session tracking must be rehosted (e.g. a custom bean backed by SessionPersistentRegistry / SecurityIdentity, or quarkus session management). The original producer was: @Bean public SessionRegistryImpl sessionRegistry() { return new SessionRegistryImpl(); }
- `app/proprietary/src/main/java/stirling/software/proprietary/security/supabase/SupabaseJwtDecoderFactory.java:12` - Produces the JWKS configuration for the proprietary Supabase login path. Only relevant when {@code security.supabase.user-login.enabled=true}. TODO: Migration required - this class previously produced a Spring Security {@code org.springframework.security.oauth2.jwt.JwtDecoder} bean (Nimbus-based) via {@code @Configuration}/{@code @Bean}, conditionally registered with {@code @ConditionalOnProperty(security.supabase.user-login.enabled=true)}. Quarkus has no {@code JwtDecoder} abstraction; beare...
- `app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java:999` - Quarkus migration: was SecurityContextHolder.getContext().getAuthentication(). TODO: Migration required - the original code distinguished API-key auth from web/JWT auth via `instanceof ApiKeyAuthenticationToken`. Under Quarkus the runtime principal is io.quarkus.security.identity.SecurityIdentity and ApiKeyAuthenticationToken has been reduced to a plain POJO (it is no longer the identity type), so the API-key vs WEB distinction can no longer be made by instanceof here. Once the API-key auth p...
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:74` - TODO: Migration required - SecurityIdentity replaces Spring's Authentication. The collaborator FileStorageService still exposes canAccessShareLink(FileShare, org.springframework.security .core.Authentication) and recordShareAccess(FileShare, Authentication, boolean). Once that service is migrated those methods should accept SecurityIdentity (or io.quarkus.security SecurityContext) and this injected identity can be passed through directly.
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:271` - TODO: Migration required - canAccessShareLink/recordShareAccess still take Spring Authentication. Passing null preserves the anonymous-deny behavior until the service is migrated to SecurityIdentity; once migrated, pass `securityIdentity` through instead.
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:296` - TODO: Migration required - canAccessShareLink still takes Spring Authentication; pass `securityIdentity` once FileStorageService is migrated.
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:380` - TODO: Migration required - Spring's Authentication-based anonymous check is replaced by SecurityIdentity. Verify "anonymous" semantics match once the security layer is migrated.
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FolderService.java:435` - TODO: Migration required - a Quarkus SecurityIdentityAugmentor/IdentityProvider must attach the stirling.software.proprietary.security.model.User entity as the SecurityIdentity principal (Spring exposed it directly via Authentication#getPrincipal, since User used to implement UserDetails). Until that augmentor exists, this only resolves when the principal IS the User entity; otherwise it rejects as 401 rather than guessing at a username->User lookup.
- `app/proprietary/src/test/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandlerTest.java:26` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/security/oauth2/TauriAuthorizationRequestResolverTest.java:15` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/security/service/CustomOAuth2UserServiceDebugLoggingTest.java:46` - Spring Boot test framework not available in Quarkus
- `app/saas/src/main/java/stirling/software/saas/security/EnhancedJwtAuthenticationToken.java:15` - JWT auth token that exposes the Supabase subject UUID and email alongside the standard claims, so downstream code (audit, credit accounting) can avoid re-parsing the JWT every request. TODO: Migration required - originally extended {@code org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken}. That Spring type has no Quarkus equivalent; it now extends the {@link AbstractAuthenticationToken} common shim and carries the {@link JsonWebToken} as token/principal...
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:84` - TODO: Migration required - original @Bean JwtDecoder jwtDecoder() built a NimbusJwtDecoder from the Supabase JWKS endpoint (issuer + "/.well-known/jwks.json") and attached a SupabaseTokenValidator (iss/exp/aud enforcement with clock skew), failing closed when the issuer was unusable. NimbusJwtDecoder / JwtDecoder are Spring OAuth2 types with no Quarkus equivalent; configure Quarkus OIDC (quarkus.oidc.auth-server-url / mp.jwt.verify.* ) to point at the Supabase JWKS instead. The issuer validat...
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:119` - Validates iss, exp (with clock-skew) and optionally aud on a decoded Supabase JWT. TODO: Migration required - originally implemented Spring's {@code OAuth2TokenValidator<Jwt>} and returned {@code OAuth2TokenValidatorResult}. Those Spring OAuth2 types are gone; the validation now operates on {@link JsonWebToken} and returns the list of error messages (empty == valid). Re-wire this into Quarkus OIDC token validation.
- `app/saas/src/main/java/stirling/software/saas/util/AuthenticationUtils.java:95` - JsonWebToken principal from the Quarkus OIDC/JWT resource server TODO: Migration required - was Spring's org.springframework.security.oauth2.jwt.Jwt; getClaimAsString("email") replaced with MicroProfile JsonWebToken.getClaim("email").
</details>
<details><summary><b>Conditional beans (@ConditionalOn*) -> runtime guards</b> (24)</summary>
- `app/common/src/main/java/stirling/software/common/cluster/inprocess/InProcessClusterConfiguration.java:22` - TODO: Migration required - the original @ConditionalOnExpression ("!${cluster.enabled:false} || '${cluster.backplane:inprocess}'.equalsIgnoreCase('inprocess')") gated activation of this whole configuration on a SpEL expression over two config properties. Quarkus/CDI has no direct equivalent for conditionally registering a producer set based on a SpEL boolean. The @DefaultBean producers below now always provide the in-process implementations unless another bean of the same type is present. If ...
- `app/common/src/main/java/stirling/software/common/cluster/inprocess/LocalDiskFileStoreConfiguration.java:17` - TODO: Migration required - the original class was guarded by Spring's @ConditionalOnProperty(prefix="cluster", name="artifactStore", havingValue="local", matchIfMissing=true). Quarkus has no runtime equivalent: @io.quarkus.arc.profile.IfBuildProperty is build-time only and does not support matchIfMissing semantics. The producer below is now unconditional. The "local is the default; S3 supplies its own bean" behavior is preserved via @DefaultBean (the S3 artifact-store bean, if present, wins o...
- `app/common/src/main/java/stirling/software/common/configuration/AppConfig.java:45` - <ul> <li>{@code @Bean} -> {@code @Produces}; {@code @Bean(name="x")} -> {@code @Produces @Named("x")}. <li>{@code @Value} -> {@code @ConfigProperty}; Spring {@code Environment} -> MicroProfile {@code Config}. <li>{@code @Profile("default")} flavor-default beans -> {@code @DefaultBean}: the :proprietary / :saas modules provide the "real" producer and automatically win when present, exactly like the old profile override (this is the Quarkus idiom for "default unless overridden"). <li>{@code @Sc...
- `app/core/src/main/java/stirling/software/SPDF/service/pdfjson/JobOwnershipServiceImpl.java:25` - MIGRATION: Spring's @ConditionalOnProperty(name="security.enable-login", havingValue="true") gated this bean. It is now @IfBuildProperty(security.enable-login=true) - the exact build-time complement of NoOpJobOwnershipService (@IfBuildProperty security.enable-login=false, enableIfMissing=true). The two are mutually exclusive at build time, so exactly one JobOwnershipService bean exists and callers can inject it directly (no Instance<> needed). A previous @LookupIfProperty here left both impls...
- `app/core/src/main/java/stirling/software/SPDF/service/pdfjson/NoOpJobOwnershipService.java:17` - TODO: Migration required - Spring's @ConditionalOnProperty(matchIfMissing=true) is a runtime condition; Quarkus @IfBuildProperty is evaluated at build time. enableIfMissing=true preserves the matchIfMissing default. If security.enable-login must be toggled at runtime, switch to @io.quarkus.arc.lookup.LookupIfProperty with Instance<JobOwnershipService> injection at use sites.
- `app/core/src/main/java/stirling/software/SPDF/service/telegram/TelegramPipelineBot.java:49` - TODO: Migration required - the original class was guarded by Spring's @ConditionalOnProperty(prefix="telegram", name="enabled", havingValue="true"). Migrated to a runtime guard: the bean is always created, but register() (the @PostConstruct startup hook) short-circuits when the bot token/username are not configured, so an unconfigured Telegram integration stays inert. This is a true runtime toggle (no build-time pinning required).
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterMetrics.java:22` - TODO: Migration required - original @ConditionalOnProperty(name = "cluster.enabled", havingValue = "true") was a runtime toggle. Quarkus @IfBuildProfile/@LookupIfProperty are build-time only. Either gate registration with a runtime guard on applicationProperties.getCluster().isEnabled() (e.g. skip meter registration when disabled), or use @io.quarkus.arc.lookup.LookupIfProperty if a build-time switch is acceptable.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterNodeBootstrap.java:46` - TODO: Migration required - Spring @ConditionalOnProperty(name = "cluster.enabled", havingValue = "true") was a runtime toggle. Quarkus build-time conditionals (@IfBuildProfile / @LookupIfProperty) cannot gate a StartupEvent observer at runtime, so the bean is always instantiated and the toggle is enforced at runtime via clusterEnabled below.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/s3/S3FileStoreConfiguration.java:18` - TODO: Migration required - the original Spring class was guarded by @ConditionalOnProperty(prefix="cluster", name="artifactStore", havingValue="s3") and @ConditionalOnMissingBean on the @Bean. The S3 producer below is gated with @io.quarkus.arc.lookup.LookupIfProperty(name="cluster.artifactStore", stringValue="s3"), which only contributes this FileStore when the property is "s3"; the always-on @DefaultBean producer in common's LocalDiskFileStoreConfiguration covers the "local"/default case, s...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ConditionalOnValkeyBackplane.java:23` - {@code @ConditionalOnExpression("${cluster.enabled:false} and '${cluster.backplane:inprocess}'.equals('valkey')")} SpEL guard. Quarkus/CDI has no SpEL-based conditional, but the boolean AND of two simple property checks maps directly onto two stacked (repeatable) {@link LookupIfProperty} annotations, which are evaluated with AND semantics. The Valkey producer beans are looked up only when both properties hold; otherwise the {@code @DefaultBean} in-process implementations win. TODO: Migration ...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:24` - TODO: Migration required - this class was built on spring-data-redis types (LettuceConnectionFactory, StringRedisTemplate, RedisStandaloneConfiguration, LettuceClientConfiguration, RedisPassword, RedisConnection) plus direct io.lettuce.core usage. Quarkus has no spring-data-redis; the backplane should be reworked onto io.quarkus.redis.datasource.RedisDataSource / ReactiveRedisDataSource configured via quarkus.redis.* in application.properties (hosts, password, tls, timeout=2s). The Spring imp...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyJobStore.java:41` - TODO: Migration required - @ConditionalOnValkeyBackplane (Spring @ConditionalOnExpression) is a runtime toggle on cluster.enabled + cluster.backplane=valkey. Quarkus has no direct equivalent for the composite expression; either reimplement ConditionalOnValkeyBackplane as a Quarkus build-time condition (@io.quarkus.arc.profile.IfBuildProfile / @io.quarkus.arc.lookup.LookupIfProperty) or guard bean activation at runtime. Annotation left in place pending that collaborator change. Build-time gati...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyKeyValueCache.java:18` - TODO: Migration required - @ConditionalOnValkeyBackplane (a Spring @ConditionalOnExpression composite on cluster.enabled + cluster.backplane=valkey) has no direct CDI equivalent. Once that collaborator annotation is migrated, re-guard this bean (e.g. @io.quarkus.arc.lookup.LookupIfProperty or @io.quarkus.arc.profile.IfBuildProfile, or a runtime guard) so Valkey beans only load when cluster.enabled=true AND cluster.backplane=valkey. Build-time gating: included in the build only when cluster.ba...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/McpServerController.java:36` - @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") -> LookupIfProperty. LookupIfProperty gates programmatic lookup; for a JAX-RS resource Quarkus always registers the endpoint. TODO: Migration required - to truly disable the /mcp route when mcp.enabled=false, add a runtime guard (e.g. reject in handle() when disabled) or use a build-time conditional; LookupIfProperty alone does not unregister the REST path.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:34` - TODO: Migration required - the original @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") gated this bean on a runtime property. Quarkus build-time conditions (@io.quarkus.arc.lookup.LookupIfProperty / @io.quarkus.arc.profile.IfBuildProfile) cannot honour a purely runtime toggle. The bean is now always present; callers must guard on applicationProperties.getMcp() / a runtime "mcp.enabled" check, or wire @LookupIfProperty on the injection points once "mcp.enabled" is promoted ...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/engine/EngineCapabilityClient.java:39` - TODO: Migration required - @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") has no direct CDI equivalent. The onReady() observer below guards on a runtime config toggle instead; consider @io.quarkus.arc.lookup.LookupIfProperty / a build-time profile if the bean itself should be excluded.
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/tools/McpOperationExecutor.java:38` - TODO: Migration required - the Spring @ConditionalOnProperty(name = "mcp.enabled", havingValue = "true") guard is not directly portable. For a build-time toggle use @io.quarkus.arc.lookup.LookupIfProperty(name = "mcp.enabled", stringValue = "true") on the injection points, or gate the call sites at runtime; this bean is otherwise always created.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/RateLimitResetScheduler.java:12` - TODO: Migration required - Spring @Profile("!saas") gated this scheduler so it never ran in the "saas" profile. @io.quarkus.arc.profile.UnlessBuildProfile("saas") reproduces this when "saas" is a Quarkus BUILD profile; if "saas" is only a runtime profile, this annotation has no effect and the body of resetRateLimit() must instead short-circuit on a runtime profile check (org.eclipse.microprofile.config Config "quarkus.profile" / ProfileManager.getActiveProfile()).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java:50` - MIGRATION NOTES (Spring -> Quarkus CDI): <ul> <li>{@code @Configuration} -> {@code @ApplicationScoped}; {@code @Bean} -> {@code @Produces}. <li>{@code @Qualifier("runningProOrHigher")} ctor param -> {@code @Inject} ctor with {@code @Named(...)} on the parameter (the producer lives in common {@code AppConfig}). <li>{@code @Profile("!saas")} on the producer -> {@code @UnlessBuildProfile("saas")} so the SaaS Postgres datasource shadows this H2 default exactly as the old profile override did. <li...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/EEAppConfig.java:31` - <ul> <li>{@code @Configuration} -> {@code @ApplicationScoped}; {@code @Bean(name="x")} -> {@code @Produces @Named("x")}. These producers deliberately omit {@code @DefaultBean} so they OVERRIDE the {@code @DefaultBean} producers declared in {@code stirling.software.common.configuration.AppConfig} whenever the :proprietary module is on the classpath - this is the Quarkus idiom for Spring's profile-based bean override. <li>{@code @Profile("security & !saas")} -> {@code @IfBuildProfile("security"...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java:31` - TODO: Migration required - Spring @ConditionalOnProperty(mail.enabled) gated bean creation. CDI has no direct runtime-toggle equivalent; this controller is always registered and instead guards at request time via the injected mail.enabled config below. If the endpoint must be fully absent when mail is disabled, wire this with @io.quarkus.arc.lookup.LookupIfProperty or a build-time @io.quarkus.arc.profile.IfBuildProfile once a build/runtime decision is made.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/api/Email.java:11` - TODO: Migration required - dropped @ConditionalOnProperty("mail.enabled"). This is a request DTO, not a CDI bean, so conditional bean registration does not apply. The mail.enabled gate must be enforced on the consuming endpoint/service (e.g. via @IfBuildProfile / LookupIfProperty or a runtime guard on the email controller), not on this model.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:21` - TODO: Migration required - the original class was guarded by @ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false). Quarkus has no @ConditionalOnProperty. mail.enabled is a runtime property (ApplicationProperties.Mail#isEnabled) rather than a build-time flag, so the bean is always produced and callers must guard on applicationProperties.getMail().isEnabled() at call time. SMTP connection settings now live under quarkus.mailer.* config instead of MailConfig.
- `app/saas/src/main/java/stirling/software/saas/security/TeamSecurityExpressions.java:27` - TODO: Migration required - @Profile("saas") had no Quarkus equivalent here; gate bean availability via build profile / @IfBuildProfile if saas-only activation is required.
</details>
<details><summary><b>Spring Data -> Panache</b> (5)</summary>
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:65` - jakarta.ws.rs.ext.ExceptionMapper}. Because JAX-RS resolves at most one mapper per exception type, this single {@code ExceptionMapper<Throwable>} reproduces the original per-type {@code @ExceptionHandler} dispatch by inspecting the thrown exception with {@code instanceof}. The RFC 7807 body, previously a Spring {@code ProblemDetail}, is now built as an ordered {@link java.util.Map} (serialized by quarkus-rest-jackson) to preserve the exact response shape without depending on Spring types. <h2...
- `app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:24` - are preserved verbatim and executed through Panache's {@link #find(String, Object...)} / {@link #find(String, io.quarkus.panache.common.Sort, java.util.Map)} APIs. TODO: Migration required - the previous Spring Data signatures returned {@code org.springframework.data.domain.Page<T>} and accepted {@code org.springframework.data.domain.Pageable}. Those Spring types are gone in Quarkus; the paged finders below now return a Panache {@link PanacheQuery} and accept an {@code io.quarkus.panache.comm...
- `app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:197` - Find IDs for batch deletion - using JPQL with paging instead of a native query. TODO: Migration required - originally accepted a Spring {@code Pageable}; callers must pass an {@code io.quarkus.panache.common.Page} instead (see class doc).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/TeamController.java:247` - TODO: Migration required - teamRepository/userRepository still extend Spring Data JpaRepository. Once they are migrated to Panache, findById(...) returns the entity directly (not Optional); update the Optional handling above accordingly. Likewise save(...) -> persist(...), delete(...) -> delete(...)/deleteById(...). Derived finders existsByNameIgnoreCase / countByTeam must be reimplemented as Panache default methods.
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/service/FolderService.java:268` - TODO: Migration required - StoredFileRepository is still a Spring Data JpaRepository; save()/saveAll()/flush() resolve against it for now. When that repository is ported to a Panache repository, map these to persist()/flush() accordingly.
</details>
<details><summary><b>MVC view / template rendering -> Qute or static</b> (14)</summary>
- `app/common/src/main/java/stirling/software/common/util/ErrorUtils.java:10` - TODO: Migration required - server-rendered error view removed; surface via JAX-RS ExceptionMapper. Spring MVC org.springframework.ui.Model has no Quarkus/Jakarta (JAX-RS) drop-in; the method now mutates and returns a plain Map<String, Object> model holder.
- `app/common/src/main/java/stirling/software/common/util/ErrorUtils.java:23` - TODO: Migration required - server-rendered error view removed; surface via JAX-RS ExceptionMapper. Spring MVC org.springframework.web.servlet.ModelAndView has no Quarkus/Jakarta (JAX-RS) drop-in; the method now returns a plain Map<String, Object> model holder instead of a ModelAndView (the incoming model parameter is retained for signature compatibility but is no longer the Spring Model type).
- `app/core/src/main/resources/application.properties:62` - ---- Error handling (was spring.web.error.* / spring.mvc.problemdetails.enabled=false) -------- TODO: Migration required - GlobalExceptionHandler is an @ControllerAdvice; rewrite as JAX-RS ExceptionMapper(s) producing RFC 7807 ProblemDetail responses. The Spring error-page / whitelabel settings below have no Quarkus property equivalent: spring.web.error.path=/error, whitelabel.enabled=false, include-stacktrace/exception/message=always
- `app/proprietary/build.gradle:16` - ---- Spring -> Quarkus extension mapping (full native migration) ---- spring-jdbc -> Agroal datasource (transitive via hibernate-orm). JdbcTemplate usage, if any, must be rewritten to plain JDBC / Panache. TODO: Migration required - replace any org.springframework.jdbc.core.JdbcTemplate usage. spring-webmvc -> quarkus-rest (inherited api-scoped from :common).
- `app/proprietary/build.gradle:30` - spring-boot-starter-data-redis -> quarkus-redis-client (used by the optional Valkey backplane). TODO: Migration required - rewrite RedisTemplate/Lettuce usage on the Quarkus Redis client API.
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditDashboardWebController.java:35` - Spring's org.springframework.ui.Model + view-name ("audit/dashboard") drove Thymeleaf server-side rendering. Quarkus has no Thymeleaf view resolver; the equivalent is a Qute TemplateInstance bound to src/main/resources/templates/audit/dashboard.html. TODO: Migration required - rebind this view to Qute. Inject @io.quarkus.qute.Location("audit/dashboard") io.quarkus.qute.Template dashboard; and return dashboard.data(...) as a TemplateInstance (with a Qute RestEasy extension), or render the page...
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditDashboardWebController.java:53` - TODO: Migration required - return the rendered Qute template instead of this placeholder once audit/dashboard.html is migrated. The attributes in `model` map 1:1 to the former Spring Model attributes.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyClusterBackplane.java:25` - TODO: Migration required - was Spring spring-data-redis StringRedisTemplate. Replaced with Quarkus RedisDataSource (io.quarkus.redis.datasource). Verify the redis client extension (quarkus-redis-client) is on the classpath and configured via quarkus.redis.* properties.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyClusterBackplane.java:37` - Original used template.execute() so the connection was borrowed from the pool and returned in a finally block - critical because isHealthy() is hit on every k8s liveness/readiness probe tick. Quarkus RedisDataSource manages connection pooling/return internally, so issuing a single command (PING) is the equivalent. TODO: Migration required - confirm command mapping. Quarkus exposes PING via the low-level command API: redisDataSource.execute("PING") returns a Response whose toString() is the si...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:86` - MIGRATION: the former @Produces RedisDataSource methods (valkeyConnectionFactory / valkeyTemplate) were removed - they only handed back the container-managed RedisDataSource and produced two @Default beans of the same type, which Arc flagged as an ambiguous dependency for every consumer that injects a plain RedisDataSource. All Valkey* collaborators now inject the Quarkus-provided RedisDataSource directly. TODO: Migration required - the eager boot handshake / URL+TLS validation that used to r...
- `app/saas/src/main/java/stirling/software/saas/ai/controller/AiProxyController.java:155` - TODO: Migration required - Spring's "/output/**" wildcard mapping has no direct JAX-RS equivalent; using a {path:.*} regex template to capture the trailing path segments.
- `app/saas/src/main/java/stirling/software/saas/config/SaasRestTemplateConfig.java:15` - HTTP client for talking to Supabase Edge Functions, with a bounded connect timeout. TODO: Migration required - replaced Spring RestTemplate with java.net.http.HttpClient. Consider a typed {@code @RegisterRestClient} client instead. Note: the per-request read timeout previously set on RestTemplate must now be applied per HttpRequest via {@code HttpRequest.Builder#timeout}.
- `app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java:43` - TODO: Migration required - Spring RestTemplate replaced with JDK java.net.http.HttpClient for the Supabase edge-function email POST (see sendInvitationEmail).
- `app/saas/src/main/java/stirling/software/saas/service/SaasTeamService.java:705` - TODO: Migration required - Spring RestTemplate (HttpHeaders/MediaType/HttpEntity + postForEntity) replaced with JDK HttpClient. Preserves the JSON POST with the bearer Authorization header to the Supabase edge function.
</details>
<details><summary><b>Spring config/env -> MicroProfile Config</b> (10)</summary>
- `app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:45` - TODO: Migration required - rebind via @io.smallrye.config.ConfigMapping or @io.quarkus.arc.config.ConfigProperties. Was Spring @ConfigurationProperties(prefix = ""), kept here as a plain CDI bean POJO; the property binding is not yet wired in Quarkus. TODO: Migration required - Spring @Order(Ordered.HIGHEST_PRECEDENCE) controlled configuration-bean ordering; there is no equivalent CDI ordering annotation for this bean.
- `app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:84` - REMOVED (Spring -> Quarkus): dynamicYamlPropertySource(ConfigurableEnvironment). This was a Spring @Bean that registered settings.yml as an extra runtime PropertySource on the ConfigurableEnvironment (added first, or last under the "saas" profile). Quarkus has no ConfigurableEnvironment/PropertySource model and the @Bean had already been removed, so the method was dead code referencing Spring-only types. TODO: Migration required - reimplement external settings.yml loading as a custom org.ecli...
- `app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java:18` - Spring Boot test framework not available in Quarkus
- `app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java:70` - TODO: Migration required - the Spring "spring.config.additional-location" property used to load the external settings/customSettings YAML files into the environment. Quarkus uses SmallRye Config; wire these files via a config source instead, e.g. set the system property "smallrye.config.locations" to the (comma-separated) file: URLs before this point, or register a custom ConfigSourceFactory. The directories/log lines above are preserved.
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:114` - TODO: Migration required - development mode used to be derived from Spring active profiles via org.springframework.core.env.Environment. Quarkus exposes the profile through io.quarkus.runtime.LaunchMode / quarkus.profile; this is read here from the standard config so no Spring Environment is needed.
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:902` - TODO: Migration required - this replaces Spring's Environment.getActiveProfiles() ("dev"/"development") check. Quarkus exposes the active profile via io.quarkus.runtime.LaunchMode and the "quarkus.profile" config key; read it from the standard config so no Spring Environment bean is required.
- `app/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java:16` - TODO: Migration required - Spring @Order(HIGHEST_PRECEDENCE + 10) had no direct CDI equivalent; bean ordering/precedence must be handled via @Priority or explicit ordering at injection points if it was relied upon.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/DatabaseController.java:44` - TODO: Migration required - @Conditional(H2SQLCondition.class) gated this controller on the datasource being H2 (driver/url inspection of the Spring Environment). Quarkus has no @Conditional equivalent; this must be re-expressed either as a build-time @IfBuildProfile, a runtime @LookupIfProperty on a datasource property, or a runtime guard inside DatabaseService that no-ops/returns 404 when the active datasource is not H2.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/supabase/SupabaseUserLoginProperties.java:8` - TODO: Migration required - this was a Spring @ConfigurationProperties(prefix = "security.supabase.user-login") POJO. Rebind the prefixed properties via @io.smallrye.config.ConfigMapping(prefix = "security.supabase.user-login") (interface-based) so the fields are populated from configuration; until then this bean holds defaults only.
- `app/saas/src/main/java/stirling/software/saas/config/SupabaseConfigurationProperties.java:11` - TODO: Migration required - @ConfigurationProperties(prefix="app.supabase"); bind via @ConfigProperty or @ConfigMapping
</details>
<details><summary><b>Spring test framework -> Quarkus test</b> (11)</summary>
- `app/common/src/test/java/stirling/software/common/cluster/InProcessConfigurationConditionalTest.java:20` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/cluster/s3/S3VendorComprehensiveTest.java:55` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveExternalClusterTest.java:41` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyAuthIntegrationTest.java:28` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyChaosTest.java:32` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/LiveValkeyIntegrationTest.java:43` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/ValkeyClusterBackplaneTest.java:25` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfigurationTest.java:33` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/mcp/security/McpApiKeyIntegrationTest.java:36` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/mcp/security/McpOAuthIntegrationTest.java:58` - Spring Boot test framework not available in Quarkus
- `app/proprietary/src/test/java/stirling/software/proprietary/security/service/MailConfigTest.java:18` - Spring Boot test framework not available in Quarkus
</details>
<details><summary><b>Scheduling / async</b> (14)</summary>
- `app/common/src/main/java/stirling/software/common/configuration/SchedulingConfig.java:15` - Quarkus' {@code quarkus-scheduler} extension owns the scheduling thread pool, so no application bean is required. To keep the "each scheduled task on its own virtual thread" behaviour, annotate the individual {@code @io.quarkus.scheduler.Scheduled} methods with {@code @io.smallrye.common.annotation.RunOnVirtualThread} (or configure {@code quarkus.scheduler.use-virtual-threads=true} where supported). TODO: Migration required - any injection point that received the former Spring {@code TaskSche...
- `app/common/src/main/java/stirling/software/common/service/TempFileCleanupService.java:132` - Scheduled task to clean up old temporary files. Runs at the configured interval. TODO: Migration required - the Spring form used a SpEL expression ({@code fixedDelayString="#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}"}). Quarkus {@code @Scheduled} cannot reference an arbitrary bean property; {@code every} only resolves a MicroProfile Config placeholder. The cleanup interval must therefore be exposed as a config key (e.g. {@code stirling.temp.cleanup-interval}) bo...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterNodeBootstrap.java:94` - TODO: Migration required - Spring @Scheduled(fixedDelayString = "${cluster.node.heartbeat-interval-ms:5000}") drove the interval directly from config in milliseconds. Quarkus @Scheduled "every" expects a Duration string, so the config reference "{cluster.node.heartbeat-interval-ms}" cannot be reused as-is (it resolves to a bare number). Hard-coded to 5s to match the model default; if the interval is operator-tunable, expose a duration-formatted property (e.g. cluster.node.heartbeat-interval=5...
- `app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:46` - TODO: Migration required - was @Async("auditExecutor") (Spring async executor). Quarkus has no @Async; run this off the request thread via a managed executor (e.g. inject org.eclipse.microprofile.context.ManagedExecutor and submit, or annotate with @io.smallrye.common.annotation.Blocking on a reactive path). Logic is kept synchronous for now to avoid changing behavior incorrectly.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.java:58` - TODO: Migration required - Spring used initialDelay=fixedRate=7d. Quarkus @Scheduled has no initialDelay equivalent for fixed-rate; "every=P7D" fires the first run 7 days after start, which preserves the original initial-delay semantics. delayed="..." could add an extra offset if needed. MIGRATION: every="7d" was rejected ("Invalid every() expression") because Quarkus parses the value as a Duration and a bare "7d" maps to the invalid "PT7d". Use the ISO-8601 period form P7D (7 days), which Du...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/database/ScheduledTasks.java:23` - TODO: Migration required - the original bean used @Conditional(H2SQLCondition.class) to skip registration entirely when not running on H2. Quarkus has no runtime @Conditional, so the gate is evaluated at runtime here via h2SQLCondition.matches() and the backup is short-circuited when false. The schedule still fires on the configured cron but becomes a no-op off H2. TODO: Migration required - the Spring cron was a SpEL expression "#{applicationProperties.system.databaseBackup.cron}". Quarkus @...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:46` - TODO: Migration required - Spring's @Async ran this on a managed executor. Quarkus has no @Async; the method now runs synchronously on the caller's thread. To restore async behaviour wrap the body in io.smallrye.mutiny.Uni or submit to a jakarta.enterprise.concurrent ManagedExecutor (would change the void signature, so deferred).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:100` - TODO: Migration required - @Async dropped (no Quarkus equivalent); now runs synchronously.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:125` - TODO: Migration required - @Async dropped (no Quarkus equivalent); now runs synchronously.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:151` - TODO: Migration required - @Async dropped (no Quarkus equivalent); now runs synchronously.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:211` - TODO: Migration required - @Async dropped (no Quarkus equivalent); now runs synchronously.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:257` - TODO: Migration required - @Async dropped (no Quarkus equivalent); now runs synchronously.
- `app/proprietary/src/main/java/stirling/software/proprietary/service/AiUserDataService.java:33` - TODO: Migration required - Spring's @Async ran this fire-and-forget on a managed executor so an unavailable engine never delayed the logout response. Quarkus has no @Async; the method now runs synchronously on the caller's thread. To restore off-thread dispatch, inject a jakarta.enterprise.concurrent.ManagedExecutorService (or annotate the calling REST endpoint with @io.smallrye.common.annotation.RunOnVirtualThread). Errors are still swallowed, so the only behavioural change is that the calle...
- `app/saas/src/main/java/stirling/software/saas/payg/job/StaleJobCloser.java:49` - TODO: Migration required - was configurable via property payg.job.stale-close-interval-ms (default 60000ms). io.quarkus.scheduler.Scheduled#every is a fixed string; restore configurability with @Scheduled(every = "{payg.job.stale-close-interval}") + a Duration config property if the interval must stay tunable.
</details>
<details><summary><b>Transactions</b> (5)</summary>
- `app/proprietary/src/main/java/stirling/software/proprietary/config/AuditJpaConfig.java:6` - TODO: Migration required - Quarkus enables transaction management automatically (Narayana/JTA via quarkus-narayana-jta); the Spring @EnableTransactionManagement is not needed. Use jakarta.transaction.@Transactional on methods/beans as required. Scheduling is enabled on the application — no duplicate @EnableScheduling needed. JPA repositories are auto-discovered by Quarkus (no @EnableJpaRepositories needed).
- `app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java:133` - TODO: Migration required - @Transactional(readOnly = true): jakarta.transaction.Transactional has no readOnly attribute; using a plain transaction.
- `app/saas/src/main/java/stirling/software/saas/ai/controller/AiCreateController.java:143` - TODO: Migration required - @Transactional(readOnly = true): jakarta.transaction.Transactional has no readOnly attribute; using a plain transaction.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:59` - TODO: Migration required - replaces Spring TransactionAspectSupport. Used to mark the current jakarta @Transactional transaction rollback-only without propagating the exception.
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:123` - Caller-fixable failures (already-accepted, expired, email mismatch, etc.). Mark the transaction for rollback so anything the service did is reversed even though we don't propagate the exception out of the @Transactional method. TODO: Migration required - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
</details>
<details><summary><b>Multipart / Resource abstractions</b> (10)</summary>
- `app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:753` - TODO: Migration required - returns org.springframework.core.io.Resource, a public signature relied on by callers. Converting to InputStream/byte[]/java.nio would ripple to those call sites, so the Spring Resource type is retained for now.
- `app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:766` - TODO: Migration required - returns org.springframework.core.io.Resource, a public signature relied on by callers. Converting to InputStream/byte[]/java.nio would ripple to those call sites, so the Spring Resource type is retained for now.
- `app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java:779` - TODO: Migration required - returns org.springframework.core.io.Resource, a public signature relied on by callers. Converting to InputStream/byte[]/java.nio would ripple to those call sites, so the Spring Resource type is retained for now.
- `app/common/src/main/java/stirling/software/common/model/MultipartFile.java:24` - service layer relies on (it exposes {@code org.jboss.resteasy.reactive.multipart.FileUpload} at the REST boundary instead). To avoid rewriting the public signatures of dozens of service and util methods across every module, this interface mirrors the subset of Spring's API that the codebase actually uses. Controllers adapt the inbound {@code FileUpload}/{@code byte[]} to one of the implementations ({@link stirling.software.common.model.multipart.ByteArrayMultipartFile}, {@link stirling.softwa...
- `app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java:30` - TODO: Migration required - MultipartFile is the constructor parameter type that must match the parent ReplaceAndInvertColorStrategy(MultipartFile, ReplaceAndInvert) constructor (not in scope for this migration). There is no JAX-RS drop-in for this widely used public signature; retained until the parent and its callers are migrated together.
- `app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java:88` - MIGRATION (Spring -> JAX-RS): adapt the inbound multipart uploads to the migration shim MultipartFile so they can be passed to the existing service layer. TODO: Migration required - PipelineProcessor.generateInputFiles still declares the Spring org.springframework.web.multipart.MultipartFile[] parameter type. When that collaborator is migrated to stirling.software.common.model.MultipartFile[], this array type lines up. Until then this controller will not compile against the processor; the ada...
- `app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbzRequest.java:30` - TODO: Migration required - controller binds this model via @BeanParam multipart. The 'fileInput' field is a raw FileUpload for form binding; the controller must adapt it to a stirling.software.common.model.MultipartFile via FileUploadMultipartFile.of(fileInput).
- `app/proprietary/src/main/java/stirling/software/proprietary/service/ByteHashFileIdStrategy.java:17` - TODO: Migration required - the FileIdStrategy interface (collaborator file) still imports org.springframework.web.multipart.MultipartFile; it must be switched to stirling.software.common.model.MultipartFile so this implementation's signature matches.
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:91` - TODO: Migration required - storeFileResponse(...) still accepts Spring org.springframework.web.multipart.MultipartFile. Migrate FileStorageService to accept stirling.software.common.model.MultipartFile, then this wrapping is type-compatible.
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:111` - TODO: Migration required - updateFileResponse(...) still accepts Spring MultipartFile; migrate FileStorageService to stirling.software.common.model.MultipartFile.
</details>
<details><summary><b>Caching</b> (2)</summary>
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/CacheConfig.java:8` - TODO: Migration required - Spring's @EnableCaching + a programmatic CaffeineCacheManager @Bean has no direct Quarkus equivalent. Quarkus caching is annotation-driven (io.quarkus.cache.@CacheResult / @CacheInvalidate / @CacheName) and configured declaratively in application.properties, e.g.: quarkus.cache.caffeine."<cache-name>".maximum-size=1000 quarkus.cache.caffeine."<cache-name>".expire-after-write=<keyRetentionDays>D quarkus.cache.caffeine."<cache-name>".metrics-enabled=true # was .record...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPersistenceService.java:54` - TODO: Migration required - Spring's CacheManager/Cache("verifyingKeys") has no direct Quarkus-cache equivalent (io.quarkus.cache.Cache cannot enumerate its values). A directly-managed Caffeine cache preserves put/get/evict semantics.
</details>
<details><summary><b>Session management</b> (1)</summary>
- `app/proprietary/build.gradle:36` - REMOVED: spring-session-core - Quarkus has no Spring Session. Server-side session state (SessionPersistentRegistry, SessionRegistry) must be rewritten on Quarkus' HTTP session (quarkus-undertow servlet session) or a custom store. TODO: Migration required - port Spring Session usage (session registry / persistence).
</details>
<details><summary><b>Other deferred migration work</b> (71)</summary>
- `app/common/build.gradle:79` - Jackson 3 (tools.jackson) - retained because ~100 files migrated to the Jackson 3 namespace under Spring Boot 4. Quarkus integrates Jackson 2 for REST bodies; Jackson 3 coexists here as a plain library so those files compile and can still build/parse JSON directly. api-scoped so downstream modules (core, proprietary, saas) that import tools.jackson inherit it. TODO: Migration required - converge the codebase on a single Jackson major version.
- `app/common/src/main/java/stirling/software/common/configuration/AppConfig.java:101` - MIGRATION: many beans inject tools.jackson.databind.ObjectMapper (Jackson 3, inherited from Spring Boot 4). Quarkus' container only produces a com.fasterxml.jackson (Jackson 2) ObjectMapper for REST (de)serialization, so the Jackson 3 type is an unsatisfied CDI dependency. This producer supplies a single application-scoped Jackson 3 mapper built the same way the codebase builds them ad hoc (JsonMapper.builder().build()). REST bodies still go through Quarkus' Jackson 2 mapper; this is only for...
- `app/common/src/main/java/stirling/software/common/model/io/Resource.java:17` - public method signatures across the codebase that accept or return {@code Resource}, this interface mirrors the subset of Spring's API the codebase actually uses ({@code getInputStream/exists/getFile/getFilename/contentLength/isFile}) together with the {@link FileSystemResource}, {@link InputStreamResource} and {@link ClassPathResource} implementations. Converting a file is then just an import swap. TODO: Migration required - longer term, prefer {@code java.nio.file.Path} / {@code InputStream...
- `app/common/src/main/java/stirling/software/common/service/InternalApiClient.java:273` - Resolve the port lazily so desktop mode dispatches to the actual bound port. TODO: Migration required - verify Quarkus exposes the bound port via config. Quarkus uses "quarkus.http.port" and, for random-port test/dev runs, "quarkus.http.test-port"; the old "local.server.port"/"server.port" keys came from Spring Boot's WebServerInitializedEvent.
- `app/common/src/main/java/stirling/software/common/service/JobQueue.java:29` - TODO: Migration required - the original class implemented Spring's SmartLifecycle, which has no direct Quarkus equivalent. start() is now driven by a StartupEvent observer and stop() by @PreDestroy. The SmartLifecycle phase/auto-startup ordering semantics (getPhase()==10) cannot be expressed in CDI; if precise startup/shutdown ordering relative to other beans is required, revisit using @Priority on the observer or @io.quarkus.runtime.Startup with an ordering strategy.
- `app/common/src/main/java/stirling/software/common/util/GeneralUtils.java:258` - ResourcePatternUtils} pattern resolver. The {@code ResourceLoader} parameter was removed. {@code file:} patterns are resolved with {@link java.nio.file.Files#list}; {@code classpath:} patterns are resolved via the classloader and only support directory resources that live on the filesystem. TODO: Migration required - {@code classpath:} resolution does not enumerate entries inside a packaged JAR. For uber-jar deployments, prefer serving these assets from {@code META-INF/resources/} or build a ...
- `app/common/src/main/java/stirling/software/common/util/SpringContextHolder.java:66` - TODO: Migration required - Spring looked up by bean name across all types; here we resolve a @Named CDI bean of Object.class. Verify named beans are registered with a matching @jakarta.inject.Named qualifier so this lookup resolves the intended bean.
- `app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java:78` - TODO: Migration required - profile auto-detection (former getActiveProfile / Spring setAdditionalProfiles) must be expressed via "quarkus.profile". The classpath-shape detection logic is retained below in getActiveProfile(); translate its result into the "quarkus.profile" system property (e.g. System.setProperty("quarkus.profile", ...)) before Quarkus.run if profile-based config layering is required.
- `app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java:171` - TODO: Migration required - the Spring "local.server.port" property exposed the actual runtime port (relevant for server.port=0 / "auto" port assignment). In Quarkus read the resolved port from config "quarkus.http.port" (or observe an HTTP-started event) and update serverPortStatic here. Falling back to the configured value for now.
- `app/core/src/main/java/stirling/software/SPDF/config/AppUpdateService.java:31` - MIGRATION: Spring's request-scoped boolean bean -> @Dependent. A CDI normal scope (@RequestScoped) requires a client proxy, which is impossible for a primitive producer ("Producer method for a normal scoped bean must not have a primitive type"). @Dependent recomputes the value at each injection point, the closest behaviour to per-request evaluation. TODO: Migration required - if true per-HTTP-request semantics are needed, wrap the value in a @RequestScoped holder object instead of producing a...
- `app/core/src/main/java/stirling/software/SPDF/config/InitialSetup.java:21` - TODO: Migration required - Spring @Order(Ordered.HIGHEST_PRECEDENCE + 1) controlled the relative order of this startup hook against other initializers. CDI StartupEvent observers have no portable total ordering; if a specific run-before/run-after relationship is required, use @Priority on the observer parameter or @Observes(during=...) and coordinate ordering across the migrated startup beans.
- `app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java:42` - TODO: Migration required - endpoint mapping was commented out in the original Spring source (the @PostMapping/@Operation were disabled), so this route remains intentionally inactive. The conversion below preserves the disabled state: routing annotations are kept commented. To enable, uncomment the JAX-RS annotations and provide a multipart-bound request. @POST @jakarta.ws.rs.Path("/print-file") @jakarta.ws.rs.Consumes(MediaType.MULTIPART_FORM_DATA) @io.swagger.v3.oas.annotations.Operation( su...
- `app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java:52` - server.servlet.context-path has no direct Quarkus equivalent (it maps to quarkus.http.root-path at build time). Kept as a configurable property so the index.html base href rewrite still works. TODO: Migration required - consider sourcing this from quarkus.http.root-path instead.
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:554` - Build the JSON body previously written directly to the servlet response when the client's Accept header could not be satisfied (Spring's {@code HttpMediaTypeNotAcceptableException}). TODO: Migration required - this path was triggered by Spring MVC content negotiation. Under Quarkus/JAX-RS the equivalent is {@code jakarta.ws.rs.NotAcceptableException}; a collaborator should register a mapper that returns this body with status 406 and Content-Type application/problem+json. The body-building log...
- `app/core/src/main/resources/application.properties:113` - ---- Jackson (was spring.jackson.*) ---------------------------------------------------------- spring.jackson.deserialization.fail-on-null-for-primitives=false TODO: Migration required - no Quarkus property for FAIL_ON_NULL_FOR_PRIMITIVES; register a CDI io.quarkus.jackson.ObjectMapperCustomizer that disables that DeserializationFeature.
- `app/core/src/main/resources/application.properties:132` - ---- External config files ------------------------------------------------------------------- TODO: Migration required - SPDFApplication injected external settings.yml / custom settings via spring.config.additional-location. Quarkus uses a different config-source mechanism (SmallRye Config / quarkus.config.locations). Port ConfigInitializer accordingly.
- `app/proprietary/build.gradle:96` - JDBC drivers via Quarkus extensions (wire into the Agroal datasource). NOTE: H2 is pinned to 2.3.232 because the on-disk file format is incompatible with 2.4.x and upgrading would break existing user databases. quarkus-jdbc-h2's BOM-managed H2 version may differ, so the explicit pin is forced below to preserve file compatibility. TODO: Migration required - verify the H2 version Quarkus resolves still reads 2.3.232 files.
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:114` - Only create the map once we know we'll use it TODO: Migration required - createBaseAuditData must accept InvocationContext (ctx) once AuditService is migrated off ProceedingJoinPoint.
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:124` - TODO: Migration required - addFileData must accept InvocationContext (ctx).
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:148` - TODO: Migration required - addMethodArguments must accept InvocationContext (ctx).
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:199` - TODO: Migration required - resolveEventType reads joinPoint.getTarget(); once AuditService is migrated it should use ctx.getTarget().getClass() instead.
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:89` - TODO: Migration required - this single {@code @AroundInvoke} replaces the five Spring {@code @Around} advices (GET/POST/PUT/DELETE/PATCH + AutoJobPostMapping) and the static-resource {@code execution(...)} advice. Because CDI cannot inspect Spring/JAX-RS mapping annotations to derive the HTTP verb at bind time, the verb is resolved from the live request ({@link HttpServletRequest#getMethod()}); if the request is unavailable (non-web invocation) it falls back to POST to mirror the most common ...
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:315` - Fallback: try JAX-RS @Path annotation on method/class; return empty string if not present TODO: Migration required - resolve path from jakarta.ws.rs.@Path on the declaring class and method once all controllers are fully on JAX-RS. The Spring fallback was removed.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterLicenseGate.java:20` - Runtime license gate for cluster mode. Cluster mode requires a SERVER or ENTERPRISE license; the SaaS flavor bypasses (no {@code runningProOrHigher} bean is published). The Valkey connection config {@code @DependsOn} this bean, so it runs before any Valkey bean is constructed. TODO: Migration required - Spring @DependsOn ordering relative to the Valkey connection config has no direct Quarkus equivalent. Ensure the Valkey/Redis bean either @Inject's this gate or that this verification still ru...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterNodeBootstrap.java:35` - Integer.MAX_VALUE} so Spring tore this bean down before {@code LettuceConnectionFactory} - deregister therefore ran while the Valkey connection was still alive. TODO: Migration required - Quarkus has no SmartLifecycle/getPhase shutdown-ordering equivalent. Startup now runs via @Observes StartupEvent and shutdown via @PreDestroy. If the Quarkus Redis/Valkey client is torn down before this bean's @PreDestroy, the deregister call may fail (it already tolerates that via TTL expiry). If strict ord...
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:69` - TODO: Migration required - in Quarkus the RedisDataSource is produced by the quarkus-redis-client extension from quarkus.redis.* config rather than constructed here. This producer simply hands back the container-managed RedisDataSource so existing @Inject points keep compiling. The URL/TLS validation that used to build the LettuceConnectionFactory is still performed (and the boot handshake attempted) so misconfiguration fails fast.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:177` - Bound every backplane command. Without this a partitioned or slow Valkey would stall hot-path calls (e.g. JobController.guardNonOwner -> jobStore.get on each request); all backplane ops are non-blocking single commands, so a short timeout is safe. TODO: Migration required - propagate this to quarkus.redis.timeout=2s.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:191` - 10 x 3s = 30s boot-time retry. Auth failures (WRONGPASS/NOAUTH/NOPERM) short-circuit immediately; only transport errors get the loop. Package-private for testing. TODO: Migration required - this previously issued PING via a spring-data-redis RedisConnection. With Quarkus it should issue {@code ds.execute("PING")} (string command). The loop structure and auth short-circuit are retained; the actual ping call is stubbed so the file compiles until the RedisDataSource command surface is wired in.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:252` - TODO: Migration required - replace with ds.execute("PING").toString() (or the typed RedisDataSource command API) once the Quarkus command surface for the backplane is wired.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:302` - MIGRATION: Bucket4j's Lettuce ProxyManager (ValkeyRateLimitStore) needs a raw io.lettuce.core.RedisClient, which Quarkus' redis extension does not expose. Produce one from the same cluster.valkey.url the rest of the backplane uses so the injection point for AbstractRedisClient resolves. Only active when the Valkey backplane is selected. TODO: Migration required - propagate password/TLS auth from the parsed endpoint onto the RedisURI once cluster.valkey credentials handling is finalised.
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyRateLimitStore.java:39` - TODO: Migration required - this previously received a spring-data-redis LettuceConnectionFactory (produced by the not-yet-migrated ValkeyConnectionConfiguration) and unwrapped its native io.lettuce.core.RedisClient. Bucket4j's Lettuce ProxyManager only needs that raw RedisClient. Once ValkeyConnectionConfiguration is migrated to a Quarkus producer (exposing a RedisClient or io.quarkus.redis.datasource.RedisDataSource), inject it here directly and drop the AbstractRedisClient unwrap below. The...
- `app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:22` - TODO: Migration required - this class implemented Spring Boot Actuator's org.springframework.boot.actuate.audit.AuditEventRepository (with @Primary). Quarkus has no Actuator equivalent, so the interface and the org.springframework.boot.actuate.audit.AuditEvent type are gone. The write side has been ported to a plain CDI bean that accepts the audit data directly (see add(...) below). Whatever Spring code previously published AuditEvents to this repository must be updated to call this bean's ad...
- `app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:90` - TODO: Migration required - repo.persist(...) depends on PersistentAuditEventRepository being migrated to a Quarkus PanacheRepository (save -> persist). Update this call once that collaborator is converted.
- `app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java:77` - SSE stream timeout (ms), long enough for multi-gigabyte PDF workflows without completing out from under the executor. Derived from {@code aiEngine.streamTimeoutSeconds}. TODO: Migration required - the JAX-RS SSE API has no per-emitter timeout equivalent to Spring's {@code SseEmitter} constructor argument. Enforce this timeout against the background orchestration task (e.g. a scheduled cancellation / Future.get with timeout) if a hard cap is required; for now it only drives the timeout error f...
- `app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java:68` - TODO: Migration required - PersistentAuditEventRepository is a collaborator that must be migrated to io.quarkus.hibernate.orm.panache.PanacheRepositoryBase<PersistentAuditEvent, Long>. Its paged finders should return io.quarkus.panache.common.PanacheQuery (or apply the Page/Sort built here) instead of org.springframework.data.domain.Page. The pagination request below is expressed with Panache Page/Sort; once the repository accepts these the .page(...)/.list()/.count()/.pageCount() calls used ...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/McpServerController.java:112` - Spring's @ExceptionHandler(HttpMessageNotReadableException.class) wrapped malformed-JSON failures as a JSON-RPC Parse error. In JAX-RS this maps to a jakarta.ws.rs.ext.ExceptionMapper provider. TODO: Migration required - move this handling to a @Provider ExceptionMapper<...> (e.g. mapping the JSON deserialization exception thrown by the Jackson MessageBodyReader) returning HTTP 400 with JsonRpcResponse.failure(null, JsonRpcError.parseError("Request body is not valid JSON")). Kept here for ref...
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:95` - TODO: Migration required - the following describe the original chain wiring so the Quarkus re-implementation can reproduce it faithfully. They are documented as notes rather than executable HttpSecurity DSL (which does not exist in Quarkus).
- `app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java:33` - TODO: Migration required - conversationHistory is a list of POJOs; RESTEasy has no form converter for AiConversationMessage. It must be received as a JSON form part (e.g. a String field parsed with ObjectMapper, or a @RestForm @PartType(APPLICATION_JSON) field) once the multipart contract for this endpoint is finalised.
- `app/proprietary/src/main/java/stirling/software/proprietary/model/api/audit/AuditDateExportRequest.java:27` - TODO: Migration required - Spring @DateTimeFormat(iso = ISO.DATE) removed; JAX-RS binds LocalDate via its default ISO-8601 (yyyy-MM-dd) ParamConverter, so ISO.DATE form values still bind. If a non-ISO format is ever needed, register a jakarta.ws.rs.ext.ParamConverter.
- `app/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java:46` - --------------------------------------------------------------------- Basic paged queries TODO: Migration required - callers must adapt to the PanacheQuery return type (see class doc). ---------------------------------------------------------------------
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java:129` - TODO: Migration required - the Spring @ConditionalOnBooleanProperty(name = "premium.enabled") gate is not expressible on a private helper under CDI. The custom-database path is already guarded at runtime by the runningProOrHigher + datasource.enableCustomDatabase checks in dataSource(); if a separate premium.enabled toggle is still required, read it via org.eclipse.microprofile.config.Config (e.g. premium.enabled) inside dataSource() before calling this method.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/MailConfig.java:16` - This configuration class used to provide the Spring JavaMailSender bean. After the Quarkus migration, mail sending is handled by Quarkus' built-in {@code io.quarkus.mailer.Mailer}, which is auto-provided by the quarkus-mailer extension and injected directly where needed (e.g. in EmailService). There is therefore no longer a producer method here. TODO: Migration required - the SMTP connection settings previously configured programmatically from {@link ApplicationProperties.Mail} (host, port, u...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/PasswordEncoderConfig.java:23` - TODO: Migration required - replace BCryptPasswordEncoder once a Quarkus-compatible BCrypt implementation is wired in (see class-level note).
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:277` - Produces the persistent remember-me token repository. TODO: Migration required - {@link JPATokenRepositoryImpl} implements the Spring Security {@code PersistentTokenRepository} interface (collaborator not yet migrated). The remember-me feature itself has no Quarkus equivalent (see class javadoc); the repository is still produced so the persistence logic is available to the reimplementation. Producer return type narrowed to the concrete class to avoid importing the Spring interface here.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java:92` - Catches any messaging exception (e.g., invalid email address, SMTP server issues). TODO: Migration required - the Spring-specific org.springframework.mail.MailSendException ("Invalid Addresses" case) was previously handled separately. Once EmailService is migrated off Spring's JavaMailSender that branch can be reintroduced with the replacement exception type.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java:258` - TODO: Migration required - Spring's SecurityContextLogoutHandler has no Quarkus equivalent. Session/logout handling must be re-implemented via the migrated session registry (expire the current session) and/or quarkus auth config.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java:346` - TODO: Migration required - Spring's SecurityContextLogoutHandler has no Quarkus equivalent. Re-implement logout via the migrated session registry / quarkus auth config.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java:391` - TODO: Migration required - Spring's SecurityContextLogoutHandler has no Quarkus equivalent. Re-implement logout via the migrated session registry / quarkus auth config.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/enterprise/DatabaseControllerEnterprise.java:24` - TODO: Migration required - @Conditional(H2SQLCondition.class) had no direct Quarkus equivalent. H2SQLCondition is an org.springframework.context.annotation.Condition that inspects active profiles and datasource URL/type at bean-registration time. Quarkus has no equivalent for an arbitrary runtime Condition deciding whether to register a JAX-RS resource. Options: gate the endpoints with @io.quarkus.arc.lookup.LookupIfProperty / @io.quarkus.arc.profile.IfBuildProfile if the H2 check can be redu...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/database/H2SQLCondition.java:10` - TODO: Migration required - this was an org.springframework.context.annotation.Condition used via @Conditional(H2SQLCondition.class) to gate bean/controller registration at startup. Quarkus has no runtime @Conditional equivalent (@io.quarkus.arc.profile.IfBuildProfile / @LookupIfProperty are build-time/property-name based and cannot replicate this composite logic). The decision logic has been preserved as a runtime-evaluable CDI bean; callers that previously used @Conditional must inject this ...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java:317` - TODO: Migration required - dropped catch for org.springframework.jdbc.datasource.init.CannotReadScriptException (Spring JDBC). Raw JDBC PreparedStatement.execute() only throws SQLException; the missing-file case is now reported via the SQLException branch above. Restore equivalent handling if a Quarkus/Hibernate script runner is introduced later.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java:511` - TODO: Migration required - dropped catch for org.springframework.jdbc.datasource.init.ScriptException (Spring JDBC). Raw JDBC PreparedStatement.execute() only throws SQLException; script errors are now logged via the SQLException branch above. Restore equivalent handling if a Quarkus/Hibernate script runner is introduced later.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPairCleanupService.java:29` - TODO: Migration required - Spring @ConditionalOnBooleanProperty("v2") dropped; the "v2" runtime toggle has no direct CDI equivalent. Guard activation via a runtime check or @io.quarkus.arc.lookup.LookupIfProperty / quarkus.scheduler config if this bean should be conditionally enabled.
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:82` - TODO: Migration required - org.springframework.context.MessageSource and LocaleContextHolder (Spring i18n) have no Quarkus equivalent on the classpath. Rebind to a Quarkus message bundle (io.quarkus.qute / @org.eclipse.microprofile.config or a jakarta.enterprise localization helper) and an explicit Locale source. The injected field is removed for now and getInvalidUsernameMessage() returns a constant fallback so the bean can be constructed; localization must be restored when the i18n layer is...
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:632` - TODO: Migration required - was messageSource.getMessage("invalidUsernameMessage", null, LocaleContextHolder.getLocale()). Spring's MessageSource / LocaleContextHolder are not on the Quarkus classpath; rebind to a Quarkus localization mechanism (message bundle + request Locale) and restore the localized lookup. Returning the message key as a fallback preserves behavior shape until i18n is ported.
- `app/proprietary/src/main/java/stirling/software/proprietary/workflow/dto/SignDocumentRequest.java:80` - TODO: Migration required - wetSignatures is a parsed list of POJOs populated by the controller/service from wetSignaturesData, not bound directly from the form; RESTEasy has no converter for WetSignatureMetadata, so it is intentionally left without @RestForm.
- `app/saas/src/main/java/stirling/software/saas/ai/service/AiCreateSessionService.java:39` - TODO: Migration required - Spring MVC RequestContextHolder/ServletRequestAttributes replaced with a CDI-injected request-scoped HttpServletRequest (quarkus-undertow). Wrapped in Instance so resolution outside an active HTTP request (e.g. scheduled/startup contexts) is a safe no-op.
- `app/saas/src/main/java/stirling/software/saas/config/SaasDataSourceConfig.java:12` - SaaS-profile Postgres datasource configuration. TODO: Migration required - datasource/JPA now configured via quarkus.datasource.* / quarkus.hibernate-orm.* in application.properties. The former Hikari-based DataSource bean (Postgres, @Primary over the OSS H2 default) translates to Quarkus config, e.g.: <pre> quarkus.datasource.db-kind=postgresql quarkus.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} quarkus.datasource.password=${SPRING_DATASOURCE_PASSWORD:} quarkus.datasource.jdbc...
- `app/saas/src/main/java/stirling/software/saas/config/SaasJpaConfig.java:10` - Previously registered the {@code :saas} module's entities and repositories with Spring Data JPA. TODO: Migration required - datasource/JPA now configured via quarkus.datasource.* / quarkus.hibernate-orm.* in application.properties. Entity scanning and repository discovery are automatic in Quarkus (Panache/Hibernate ORM), so the former @EnableJpaRepositories basePackages (stirling.software.saas.repository, .billing.repository, .ai.repository, .payg.repository) and @EntityScan packages (.model,...
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:131` - TODO: Migration required - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:418` - TODO: Migration required - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:426` - TODO: Migration required - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:467` - TODO: Migration required - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:475` - TODO: Migration required - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
- `app/saas/src/main/java/stirling/software/saas/payg/lineage/LineagePruneScheduler.java:47` - TODO: Migration required - Spring 6-field cron "0 0 * * * *" (top of every hour) translated to Quartz cron "0 0 * ? * *" (day-of-month set to ? per Quartz day-of-week/day-of-month mutual-exclusion). Configurability is preserved via the {payg.lineage.prune-cron} config expression; set that property to a Quartz-syntax cron (default below) to override.
- `app/saas/src/main/java/stirling/software/saas/payg/policy/PolicyChangedEvent.java:12` - TODO: Migration required - was a Spring ApplicationEvent subclass. Converted to a plain POJO CDI event (no `extends ApplicationEvent`, no super(source) call). The `source` is retained as a plain field so the existing (Object source, String payload) constructor used by PricingPolicyService stays source-compatible.
- `app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java:198` - TODO: Migration required - after-commit delivery is now expressed on the observer side via CDI's TransactionPhase.AFTER_SUCCESS (replacing the firing-side TransactionSynchronizationManager.registerSynchronization afterCommit hook). When no transaction is active (e.g. test paths calling write methods without a tx), CDI delivers the event immediately, matching the former else-branch behavior.
- `app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicyService.java:227` - TODO: Migration required - was TransactionSynchronizationManager-driven after-commit dispatch; now a plain Event.fire() whose after-commit timing is enforced by the AFTER_SUCCESS observer phase on onPolicyChanged.
- `app/saas/src/main/java/stirling/software/saas/payg/test/PaygCucumberThrowController.java:65` - TODO: Migration required - declared return type was ResponseEntity<Void> so the AutoJobAspect @Around 500 reached the wire (see class javadoc). Verify the JAX-RS return-value handling of Response preserves the advice's 500 status under Quarkus.
- `app/saas/src/main/java/stirling/software/saas/service/SupabaseUserService.java:45` - TODO: Migration required - Spring Data save() did an upsert (merge); SupabaseUser uses an assigned UUID id and this path updates an existing row, so use EntityManager.merge to preserve update-or-insert semantics rather than Panache persist (INSERT-only).
- `build.gradle:275` - Jackson integration for JAX-RS bodies. Quarkus integrates Jackson 2 (com.fasterxml). TODO: Migration required - 100 files import tools.jackson (Jackson 3, from Spring Boot 4). Jackson 2 and 3 can coexist (different namespaces); the Jackson 3 dependency is retained in app/common so those files still compile, but REST (de)serialization goes through Quarkus' Jackson 2 ObjectMapper. Converge on one Jackson line later.
</details>
## Disabled tests
16 test classes carry `@Disabled("Spring Boot test framework not available in Quarkus")`. They need
rewriting against `@QuarkusTest` / `@QuarkusComponentTest`, or plain JUnit where the test does not
actually need a container. Find them with:
```bash
grep -rn 'Spring Boot test framework not available in Quarkus' app/
```
The root `build.gradle` additionally excludes, from the `test` source set, any test whose text
contains `import org.springframework` or `import com.nimbusds`, plus an explicit
`quarkusMigrationExcludedTests` list. Two gaps in that filter to know about:
- It matches on the **import line only**. A test that references `org.springframework.mock.web`
fully qualified, or breaks via `org.springdoc`, or reflects onto a field whose type the migration
changed, sails straight through and has to be added to the explicit list by hand.
- Because the exclusion is on the source set, an excluded test is not compiled either - so
`compileTestJava` passing is not evidence that the test still matches the code.
Both mechanisms should be deleted once the list is empty.
## Known functional gaps
- **`@ToolIO` is missing from the build-time OpenAPI schema.** `ToolIOOperationCustomizer` is an
`OASFilter` that runs at `RUNTIME_STARTUP`, because it reads `ToolIORegistry` and that only exists
once the container is up. The schema exported during augmentation
(`quarkus.smallrye-openapi.store-schema-directory`) therefore carries no `x-stirling-io`, which is
what the frontend type generator and the AI engine's `generate_tool_models.py` read. Reading the
declarations from the Jandex index in a build step would restore it.
- **Endpoint enumeration discovers nothing.** `EndpointInspector`, `McpToolCatalog` and
`AiEngineEndpointResolver` all relied on Spring's `RequestMappingHandlerMapping` to list handlers,
and currently fall back to wildcards or empty catalogues. `ToolIORegistry` shows a working
replacement on Quarkus: walk the CDI beans via `BeanManager`, read the JAX-RS `@Path` annotations
off the class and its methods. The same approach fits all three.
- **Only one `OASFilter` can be registered through `mp.openapi.filter`.** `application.properties`
uses it for `ToolModelSchemaCustomizer`, so `OpenApiConfig` and `GlobalErrorResponseCustomizer`
are ported but never invoked - the API `Info`, the global `AI` tag, the `apiKey` security scheme
and the shared error responses are all absent from the published spec. Register the extra filters
with `@io.quarkus.smallrye.openapi.OpenApiFilter` instead.
- **Fingerprint-based session management is gone.** All three classes in
`app/core/src/main/java/stirling/software/SPDF/config/fingerprint/` are commented out top to
bottom (`FingerprintGenerator`, `FingerprintBasedSessionFilter`, `FingerprintBasedSessionManager`)
because they were a Spring `@Component` filter over `HttpServletRequest`. Either port the filter
to a JAX-RS `ContainerRequestFilter` or delete the files - right now they read as live code.
- **`/api/v1/convert/pdf/video` is commented out.** The whole handler in
`ConvertPdfToVideoController` sits inside a `/* ... */` block; the class compiles but exposes no
endpoint. It needs the same `@RestForm` treatment as its neighbours before the route comes back.
- **Request models are not `@BeanParam`-bindable.** The migrated controllers rebuild their request
DTO field by field from `@RestForm` parameters, because the DTOs (`PDFFile`, `GeneralFile`,
`PdfVectorExportRequest`, `ConvertPdfToEpubRequest`, ...) carry no JAX-RS multipart annotations.
Annotating the models and switching to `@BeanParam` would delete a lot of that boilerplate.
- **`saas` flavor unmeasured.** It builds on `:proprietary` and cannot compile before that does.
+6
View File
@@ -194,6 +194,12 @@ tasks:
- task: frontend:tool-models
- task: engine:tool-models
tool-models:check:
desc: "Fail if any committed API model is out of date"
cmds:
- task: frontend:tool-models:check
- task: engine:tool-models:check
# ============================================================
# Quality Gate
# ============================================================
+12
View File
@@ -36,6 +36,14 @@
"moduleName": ".*",
"moduleLicense": "BSD-4 License"
},
{
"moduleName": ".*",
"moduleLicense": "Revised BSD"
},
{
"moduleName": ".*",
"moduleLicense": "ISC"
},
{
"moduleName": ".*",
"moduleLicense": "MIT"
@@ -52,6 +60,10 @@
"moduleName": ".*",
"moduleLicense": "MIT-0"
},
{
"moduleName": ".*",
"moduleLicense": "MIT license"
},
{
"moduleName": "com.github.jai-imageio:jai-imageio-core",
"moduleLicense": "LICENSE.txt"
+1 -4
View File
@@ -8,8 +8,7 @@ dependencies {
api 'io.quarkus:quarkus-rest-jackson'
// Servlet bridge: large amounts of controller/filter code use jakarta.servlet (HttpServletRequest,
// Filter, etc.). quarkus-undertow provides a servlet container on Quarkus so that API resolves and
// runs. TODO: Migration required - longer term, port servlet usage to JAX-RS (ContainerRequestContext)
// and drop quarkus-undertow.
// runs.
api 'io.quarkus:quarkus-undertow'
// Bean Validation (was transitively in spring-boot-starter-webmvc).
api 'io.quarkus:quarkus-hibernate-validator'
@@ -24,7 +23,6 @@ dependencies {
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46'
// REMOVED: spring-boot-starter-aspectj. Quarkus has no AspectJ weaving; quarkus-arc provides
// CDI interceptors (@AroundInvoke / interceptor bindings) instead.
// TODO: Migration required - any @Aspect/@Around advice must be rewritten as CDI interceptors.
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
@@ -76,7 +74,6 @@ dependencies {
// under Spring Boot 4. Quarkus integrates Jackson 2 for REST bodies; Jackson 3 coexists here as a
// plain library so those files compile and can still build/parse JSON directly.
// api-scoped so downstream modules (core, proprietary, saas) that import tools.jackson inherit it.
// TODO: Migration required - converge the codebase on a single Jackson major version.
api 'tools.jackson.core:jackson-databind:3.0.0'
api 'tools.jackson.core:jackson-core:3.0.0'
@@ -19,15 +19,6 @@ import stirling.software.common.model.ApplicationProperties;
* Default cluster backplane wiring: every interface gets an {@code InProcess*} bean. Active when
* cluster mode is off or {@code cluster.backplane=inprocess}.
*/
// TODO: Migration required - the original @ConditionalOnExpression
// ("!${cluster.enabled:false} || '${cluster.backplane:inprocess}'.equalsIgnoreCase('inprocess')")
// gated activation of this whole configuration on a SpEL expression over two config properties.
// Quarkus/CDI has no direct equivalent for conditionally registering a producer set based on a
// SpEL boolean. The @DefaultBean producers below now always provide the in-process implementations
// unless another bean of the same type is present. If a non-inprocess backplane is added, ensure
// it is NOT a @DefaultBean so it wins, and consider gating with
// @io.quarkus.arc.lookup.LookupIfProperty
// / @io.quarkus.arc.lookup.LookupUnlessProperty or a build-time @IfBuildProperty per producer.
@Slf4j
@ApplicationScoped
public class InProcessClusterConfiguration {
@@ -14,14 +14,6 @@ import stirling.software.common.cluster.FileStore;
* cluster.artifactStore=local} (the default; {@code matchIfMissing=true}). The S3 artifact-store
* supplies its own bean when {@code cluster.artifactStore=s3}.
*/
// TODO: Migration required - the original class was guarded by Spring's
// @ConditionalOnProperty(prefix="cluster", name="artifactStore", havingValue="local",
// matchIfMissing=true). Quarkus has no runtime equivalent: @io.quarkus.arc.profile.IfBuildProperty
// is build-time only and does not support matchIfMissing semantics. The producer below is now
// unconditional. The "local is the default; S3 supplies its own bean" behavior is preserved via
// @DefaultBean (the S3 artifact-store bean, if present, wins over this default). If a true
// runtime toggle on cluster.artifactStore is needed, gate the producer body on the config value
// and return/short-circuit accordingly.
@ApplicationScoped
public class LocalDiskFileStoreConfiguration {
@@ -0,0 +1,138 @@
package stirling.software.common.config.swagger;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.PathItem;
import io.quarkus.smallrye.openapi.OpenApiFilter;
import jakarta.enterprise.inject.spi.CDI;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.model.tool.ToolIOSpec;
import stirling.software.common.service.ToolIORegistry;
/**
* Publishes each {@link ToolIO} into the spec as {@code x-stirling-io}, which is how the frontend
* and the AI engine get it.
*
* <p>Also appends the {@code Input:/Output:/Type:} line the docs used to carry by hand, so the
* published text is unchanged without anyone maintaining it.
*
* <p>MIGRATION (Spring -> Quarkus): this was a springdoc {@code GlobalOperationCustomizer} + {@code
* GlobalOpenApiCustomizer}, which received the {@code HandlerMethod} for each operation and could
* read {@code @ToolIO} straight off it. A MicroProfile {@link OASFilter} sees only the document, so
* the declarations are looked up by path through {@link ToolIORegistry}. That registry is only
* populated once the container is up, hence {@code RUNTIME_STARTUP} - the schema exported at build
* time by {@code quarkus.smallrye-openapi.store-schema-directory} therefore carries no {@code
* x-stirling-io}.
*/
@OpenApiFilter(stages = OpenApiFilter.RunStage.RUNTIME_STARTUP)
public class ToolIOOperationCustomizer implements OASFilter {
public static final String EXTENSION_NAME = "x-stirling-io";
public static final String VOCABULARY_EXTENSION_NAME = "x-stirling-io-vocabulary";
@Override
public void filterOpenAPI(OpenAPI openApi) {
addVocabulary(openApi);
ToolIORegistry registry = registry();
if (registry == null || openApi.getPaths() == null) {
return;
}
Map<String, PathItem> pathItems = openApi.getPaths().getPathItems();
if (pathItems == null) {
return;
}
pathItems.forEach((path, item) -> registry.find(path).ifPresent(spec -> apply(item, spec)));
}
// Published separately from the declarations: generators need the full vocabulary for their
// enums, and deriving it from what is present would shrink it when an endpoint is disabled.
private static void addVocabulary(OpenAPI openApi) {
Map<String, Object> vocabulary = new LinkedHashMap<>();
vocabulary.put("formats", names(ToolFormat.values()));
vocabulary.put("arities", names(ToolArity.values()));
openApi.addExtension(VOCABULARY_EXTENSION_NAME, vocabulary);
}
private static ToolIORegistry registry() {
// OASFilter instances are created by smallrye-openapi, not by CDI, so resolve the
// registry programmatically rather than via constructor injection.
try {
return CDI.current().select(ToolIORegistry.class).get();
} catch (RuntimeException e) {
// No container (build-time schema export): publish the vocabulary only.
return null;
}
}
private static void apply(PathItem item, ToolIOSpec spec) {
if (item.getOperations() == null) {
return;
}
for (Operation operation : item.getOperations().values()) {
operation.addExtension(EXTENSION_NAME, toExtension(spec));
operation.setDescription(appendSummaryLine(operation.getDescription(), spec));
}
}
private static Map<String, Object> toExtension(ToolIOSpec spec) {
Map<String, Object> extension = new LinkedHashMap<>();
extension.put("accepts", names(spec.accepts().toArray(ToolFormat[]::new)));
extension.put("produces", spec.produces().name());
extension.put("arity", spec.arity().name());
if (!spec.cases().isEmpty()) {
extension.put("cases", cases(spec));
}
return extension;
}
private static List<Map<String, Object>> cases(ToolIOSpec spec) {
return spec.cases().stream().map(ToolIOOperationCustomizer::toCase).toList();
}
private static Map<String, Object> toCase(ToolIOSpec.Case rule) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put(
"when", rule.when().stream().map(ToolIOOperationCustomizer::toCondition).toList());
entry.put("produces", rule.produces().name());
entry.put("arity", rule.arity().name());
return entry;
}
private static Map<String, Object> toCondition(ToolIOSpec.When condition) {
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("param", condition.param());
entry.put("matches", List.copyOf(condition.matches()));
return entry;
}
private static List<String> names(Enum<?>[] values) {
return Arrays.stream(values).map(Enum::name).toList();
}
private static String appendSummaryLine(String description, ToolIOSpec spec) {
String summary =
"Input:"
+ String.join("/", names(spec.accepts().toArray(ToolFormat[]::new)))
+ " Output:"
+ spec.produces().name()
+ " Type:"
+ spec.arity().name();
if (description == null || description.isBlank()) {
return summary;
}
String trimmed = description.trim();
// The filter may see an already-published document; appending twice would double the line.
return trimmed.endsWith(summary) ? trimmed : trimmed + " " + summary;
}
}
@@ -42,11 +42,7 @@ import stirling.software.common.model.ApplicationProperties;
* scopes (e.g. {@code @RequestScoped}) require a client proxy, which is impossible for
* primitives/finals, so Spring's request-scoped primitive beans cannot be reproduced
* directly. {@code @Dependent} recomputes the value at each injection point, which is the
* closest behaviour. TODO: Migration required - if true per-HTTP-request semantics are
* needed, wrap the value in a {@code @RequestScoped} holder object instead of producing a
* bare boolean.
* <li>{@code @Lazy} dropped - CDI beans are initialised lazily by default.
* </ul>
* closest behaviour.
*/
@Slf4j
@ApplicationScoped
@@ -98,7 +94,6 @@ public class AppConfig {
// way the codebase builds them ad hoc (JsonMapper.builder().build()). REST bodies still go
// through Quarkus' Jackson 2 mapper; this is only for code that uses the Jackson 3 API
// directly.
// TODO: Migration required - converge the codebase on one Jackson line (drop Jackson 3) later.
@Produces
@ApplicationScoped
public tools.jackson.databind.ObjectMapper jackson3ObjectMapper() {
@@ -11,9 +11,5 @@ package stirling.software.common.configuration;
* the individual {@code @io.quarkus.scheduler.Scheduled} methods with
* {@code @io.smallrye.common.annotation.RunOnVirtualThread} (or configure {@code
* quarkus.scheduler.use-virtual-threads=true} where supported).
*
* <p>TODO: Migration required - any injection point that received the former Spring {@code
* TaskScheduler} bean must be rewritten to use the Quarkus scheduler API or a CDI-managed {@code
* java.util.concurrent.ScheduledExecutorService}.
*/
public class SchedulingConfig {}
@@ -42,11 +42,6 @@ import stirling.software.common.util.ValidationUtils;
@Data
@Slf4j
@ApplicationScoped
// TODO: Migration required - rebind via @io.smallrye.config.ConfigMapping or
// @io.quarkus.arc.config.ConfigProperties. Was Spring @ConfigurationProperties(prefix = ""),
// kept here as a plain CDI bean POJO; the property binding is not yet wired in Quarkus.
// TODO: Migration required - Spring @Order(Ordered.HIGHEST_PRECEDENCE) controlled
// configuration-bean ordering; there is no equivalent CDI ordering annotation for this bean.
public class ApplicationProperties {
private Legal legal = new Legal();
@@ -81,11 +76,6 @@ public class ApplicationProperties {
// ConfigurableEnvironment (added first, or last under the "saas" profile). Quarkus has no
// ConfigurableEnvironment/PropertySource model and the @Bean had already been removed, so the
// method was dead code referencing Spring-only types.
// TODO: Migration required - reimplement external settings.yml loading as a custom
// org.eclipse.microprofile.config.spi.ConfigSource (registered via a ConfigSourceProvider /
// META-INF/services), giving it an ordinal that reproduces the old precedence: higher than the
// application defaults normally, but lower than application-saas.properties under the saas
// profile. Wire it in ConfigInitializer.
/**
* Initialize fileUploadLimit from environment variables if not set in settings.yml. Supports
@@ -223,6 +213,21 @@ public class ApplicationProperties {
*/
private boolean allowPrivateS3Endpoints = false;
/**
* Whether a network source's host (SFTP, FTP, or SMB) may resolve to a loopback,
* link-local, or private address. Off by default so a connection cannot be pointed at
* internal services; enable for an on-network file server (e.g. an internal SFTP drop or a
* Samba share).
*/
private boolean allowPrivateNetworkSources = false;
/**
* Hostnames (exact, case-insensitive) that a network source may use even when they resolve
* to a private or local address and {@code allowPrivateNetworkSources} is off. Lets shared
* infra allow one named on-prem file server without opening every internal host.
*/
private List<String> allowedPrivateNetworkHosts = new java.util.ArrayList<>();
/**
* Whether an API/Purview/ConsignO integration's base URL may resolve to a loopback,
* link-local, or private address. Off by default: unlike S3 connections, any user may
@@ -735,9 +740,6 @@ public class ApplicationProperties {
}
}
// TODO: Migration required - returns org.springframework.core.io.Resource, a public
// signature relied on by callers. Converting to InputStream/byte[]/java.nio would
// ripple to those call sites, so the Spring Resource type is retained for now.
@JsonIgnore
public Resource getSpCert() {
if (spCert == null) return null;
@@ -748,9 +750,6 @@ public class ApplicationProperties {
}
}
// TODO: Migration required - returns org.springframework.core.io.Resource, a public
// signature relied on by callers. Converting to InputStream/byte[]/java.nio would
// ripple to those call sites, so the Spring Resource type is retained for now.
@JsonIgnore
public Resource getIdpCert() {
if (idpCert == null) return null;
@@ -761,9 +760,6 @@ public class ApplicationProperties {
}
}
// TODO: Migration required - returns org.springframework.core.io.Resource, a public
// signature relied on by callers. Converting to InputStream/byte[]/java.nio would
// ripple to those call sites, so the Spring Resource type is retained for now.
@JsonIgnore
public Resource getPrivateKey() {
if (privateKey == null) return null;
@@ -20,9 +20,6 @@ import stirling.software.common.model.io.Resource;
* the implementations ({@link stirling.software.common.model.multipart.ByteArrayMultipartFile},
* {@link stirling.software.common.model.multipart.FileUploadMultipartFile}) and pass it down
* unchanged.
*
* <p>TODO: Migration required - longer term, the REST boundary should standardise on {@code
* FileUpload}/{@code @RestForm} and this shim can be retired.
*/
public interface MultipartFile {
@@ -13,9 +13,6 @@ import java.io.InputStream;
* getInputStream/exists/getFile/getFilename/contentLength/isFile}) together with the {@link
* FileSystemResource}, {@link InputStreamResource} and {@link ClassPathResource} implementations.
* Converting a file is then just an import swap.
*
* <p>TODO: Migration required - longer term, prefer {@code java.nio.file.Path} / {@code
* InputStream} directly at the boundaries and retire this shim.
*/
public interface Resource {
@@ -0,0 +1,24 @@
package stirling.software.common.model.tool;
/**
* How many files an endpoint consumes and produces (Single/Multiple In, Single/Multiple Out).
*
* <p>This axis carries ZIP-as-transport: a multi-output endpoint returns its results zipped and the
* caller unpacks them, so {@code split-pages} is {@code produces = PDF, arity = SIMO} rather than
* naming a ZIP-of-PDF format. An endpoint whose deliverable really is an archive declares {@link
* ToolFormat#ZIP} with a single-output arity and stays packed.
*/
public enum ToolArity {
SISO,
SIMO,
MISO,
MIMO;
public boolean isMultiInput() {
return this == MISO || this == MIMO;
}
public boolean isMultiOutput() {
return this == SIMO || this == MIMO;
}
}
@@ -0,0 +1,47 @@
package stirling.software.common.model.tool;
/**
* One problem found while checking a chain, against the step that cannot run. {@code code} is
* stable so the frontend can pick its own wording; {@code message} is an English fallback.
*/
public record ToolDiagnostic(int stepIndex, Severity severity, String code, String message) {
public enum Severity {
/** The chain cannot run as configured. Only this should block a save. */
ERROR,
/** May not run, depending on configuration or file content. */
WARN,
/** Worth knowing but not a problem, such as a step running once per file. */
INFO
}
/** The step declares no {@link ToolIO}, so nothing past it can be checked. */
public static final String UNDECLARED = "undeclared-operation";
/** The previous step's output is not a format this step accepts. */
public static final String FORMAT_MISMATCH = "format-mismatch";
/** The previous step's output depends on a parameter that is not set yet. */
public static final String OUTPUT_UNCERTAIN = "output-uncertain";
/** The pipeline's input files are not a format the first step accepts. */
public static final String SOURCE_MISMATCH = "source-mismatch";
/** The previous step emits several files and this one runs once per file. */
public static final String FAN_OUT = "fan-out";
/** The previous step emits several files and this one consumes them in a single call. */
public static final String FAN_IN = "fan-in";
public static ToolDiagnostic error(int stepIndex, String code, String message) {
return new ToolDiagnostic(stepIndex, Severity.ERROR, code, message);
}
public static ToolDiagnostic warn(int stepIndex, String code, String message) {
return new ToolDiagnostic(stepIndex, Severity.WARN, code, message);
}
public static ToolDiagnostic info(int stepIndex, String code, String message) {
return new ToolDiagnostic(stepIndex, Severity.INFO, code, message);
}
}
@@ -0,0 +1,62 @@
package stirling.software.common.model.tool;
import java.util.List;
import lombok.Getter;
/**
* The kind of file a tool endpoint consumes or produces.
*
* <p>Encryption is its own format rather than a separate attribute, so the endpoints accepting only
* {@link #PDF} reject an encrypted one without declaring anything.
*
* <p>Extensions are a lossy projection used for run-time file checks: {@link #PDF} and {@link
* #PDF_ENCRYPTED} share {@code pdf}, because a filename cannot tell you whether a PDF is encrypted.
*/
@Getter
public enum ToolFormat {
PDF("pdf"),
PDF_ENCRYPTED("pdf"),
// Vector formats are folded in: the extension set has always included svg/eps, and splitting
// them out would make chains that run fine today report as broken.
IMAGE("png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff", "svg", "psd", "ai", "eps"),
/**
* An archive that is itself the deliverable. Multiple results use {@link ToolArity} instead.
*/
ZIP("zip", "rar", "7z", "tar", "gz", "bz2", "xz", "lz", "lzma", "z"),
WORD("doc", "docx", "odt", "rtf"),
PPT("ppt", "pptx", "odp"),
EXCEL("xls", "xlsx", "ods"),
CSV("csv"),
HTML("html", "htm", "xhtml"),
XML("xml", "xsd", "xsl"),
JSON("json"),
TEXT("txt", "text", "md", "markdown"),
MARKDOWN("md", "markdown"),
JAVASCRIPT("js", "jsx"),
EBOOK("epub", "mobi", "azw3", "fb2", "txt", "docx"),
EMAIL("eml", "msg"),
POSTSCRIPT("ps", "eps"),
PCL("pcl", "pxl"),
XPS("xps", "oxps"),
VIDEO("mp4", "webm", "avi", "mov", "mkv"),
CBZ("cbz"),
CBR("cbr"),
/** Never reported as incompatible. */
ANY(),
/** A report or a status rather than a document. */
NONE();
private final List<String> extensions;
ToolFormat(String... extensions) {
this.extensions = List.of(extensions);
}
}
@@ -0,0 +1,30 @@
package stirling.software.common.model.tool;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* What a tool endpoint consumes and produces, so a chain of steps can be checked before it runs.
*
* <p>The single source of truth: read off the handler method by {@code ToolIORegistry}, published
* into the OpenAPI spec as {@code x-stirling-io}, and generated from there into the frontend and
* the AI engine.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToolIO {
/** Defaulting to a plain PDF is what makes an ordinary endpoint reject an encrypted one. */
ToolFormat[] accepts() default {ToolFormat.PDF};
ToolFormat produces();
ToolArity arity() default ToolArity.SISO;
/** Overrides for an output that depends on a parameter; first match wins. */
ToolIOCase[] cases() default {};
}
@@ -0,0 +1,22 @@
package stirling.software.common.model.tool;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* An output that applies when every condition in {@link #when()} holds.
*
* <p>Conditions are ANDed because the interesting branches turn on more than one parameter: Add
* Password only leaves the document unencrypted when both passwords are absent.
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToolIOCase {
ToolIOWhen[] when();
ToolFormat produces();
ToolArity arity();
}
@@ -0,0 +1,19 @@
package stirling.software.common.model.tool;
import java.util.Map;
import java.util.Optional;
/**
* Supplies the {@link ToolIO} declaration for an endpoint path. An interface so a chain can be
* checked against a fixed set of declarations without standing up an application context.
*/
@FunctionalInterface
public interface ToolIOSource {
Optional<ToolIOSpec> find(String operationPath);
static ToolIOSource of(Map<String, ToolIOSpec> specs) {
Map<String, ToolIOSpec> copy = Map.copyOf(specs);
return path -> Optional.ofNullable(copy.get(path));
}
}
@@ -0,0 +1,108 @@
package stirling.software.common.model.tool;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** The runtime form of a {@link ToolIO} declaration, read off a handler method once at startup. */
public record ToolIOSpec(
Set<ToolFormat> accepts, ToolFormat produces, ToolArity arity, List<Case> cases) {
public record When(String param, List<String> matches) {
boolean holdsFor(Object value) {
String normalised = normalise(value);
return matches.stream().anyMatch(match -> normalise(match).equals(normalised));
}
}
/**
* Both sides of a condition are normalised at comparison, not at construction: the declaration
* reaches the frontend and the engine as published data, and normalising only one side there
* would silently disagree with this one.
*/
public static String normalise(Object value) {
return value == null ? "" : String.valueOf(value).trim().toLowerCase(Locale.ROOT);
}
public record Case(List<When> when, ToolFormat produces, ToolArity arity) {
public Case {
when = List.copyOf(when);
}
}
/** {@code certain} is false when a {@link Case} keys on a parameter whose value is unknown. */
public record Output(ToolFormat format, ToolArity arity, boolean certain) {}
public ToolIOSpec {
accepts = Set.copyOf(accepts);
cases = List.copyOf(cases);
}
public static ToolIOSpec from(ToolIO annotation) {
return new ToolIOSpec(
new LinkedHashSet<>(Arrays.asList(annotation.accepts())),
annotation.produces(),
annotation.arity(),
Arrays.stream(annotation.cases()).map(ToolIOSpec::toCase).toList());
}
private static Case toCase(ToolIOCase rule) {
List<When> when = Arrays.stream(rule.when()).map(ToolIOSpec::toWhen).toList();
return new Case(when, rule.produces(), rule.arity());
}
private static When toWhen(ToolIOWhen condition) {
return new When(condition.param(), List.of(condition.matches()));
}
/**
* First matching {@link Case} wins. If none match but one reads a parameter we cannot see, the
* declared output comes back uncertain: a value we never saw might have picked another branch.
*
* @param parameters the step's configured parameters, or null when not known
*/
public Output resolveOutput(Map<String, Object> parameters) {
boolean sawUnknownParam = false;
for (Case rule : cases) {
boolean allHold = true;
for (When condition : rule.when()) {
if (parameters == null || !parameters.containsKey(condition.param())) {
sawUnknownParam = true;
allHold = false;
continue;
}
allHold &= condition.holdsFor(parameters.get(condition.param()));
}
if (allHold) {
return new Output(rule.produces(), rule.arity(), true);
}
}
return new Output(produces, arity, !sawUnknownParam);
}
public Output resolveOutput() {
return resolveOutput(null);
}
public boolean acceptsFormat(ToolFormat format) {
return format == ToolFormat.ANY
|| accepts.contains(ToolFormat.ANY)
|| accepts.contains(format);
}
/** For run-time file checks. Empty means anything is accepted. */
public List<String> acceptedExtensions() {
if (accepts.contains(ToolFormat.ANY)) {
return List.of();
}
return accepts.stream()
.flatMap(format -> format.getExtensions().stream())
.distinct()
.toList();
}
}
@@ -0,0 +1,18 @@
package stirling.software.common.model.tool;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** One condition on a request parameter, guarding a {@link ToolIOCase}. */
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToolIOWhen {
String param();
/**
* Compared as strings, case-insensitively. An empty string matches an absent or blank value.
*/
String[] matches();
}
@@ -270,9 +270,6 @@ public class InternalApiClient {
private String getBaseUrl() {
// Resolve the port lazily so desktop mode dispatches to the actual bound port.
// TODO: Migration required - verify Quarkus exposes the bound port via config. Quarkus uses
// "quarkus.http.port" and, for random-port test/dev runs, "quarkus.http.test-port"; the old
// "local.server.port"/"server.port" keys came from Spring Boot's WebServerInitializedEvent.
String port = config.getOptionalValue("quarkus.http.port", String.class).orElse(null);
if (port == null) {
port = config.getOptionalValue("server.port", String.class).orElse("8080");
@@ -26,11 +26,6 @@ import stirling.software.common.util.SpringContextHolder;
* Manages a queue of jobs with dynamic sizing based on system resources. Used when system resources
* are limited to prevent overloading.
*/
// TODO: Migration required - the original class implemented Spring's SmartLifecycle, which has no
// direct Quarkus equivalent. start() is now driven by a StartupEvent observer and stop() by
// @PreDestroy. The SmartLifecycle phase/auto-startup ordering semantics (getPhase()==10) cannot be
// expressed in CDI; if precise startup/shutdown ordering relative to other beans is required,
// revisit using @Priority on the observer or @io.quarkus.runtime.Startup with an ordering strategy.
@ApplicationScoped
@Slf4j
public class JobQueue {
@@ -126,16 +126,7 @@ public class TempFileCleanupService {
}
}
/**
* Scheduled task to clean up old temporary files. Runs at the configured interval.
*
* <p>TODO: Migration required - the Spring form used a SpEL expression ({@code
* fixedDelayString="#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}"}).
* Quarkus {@code @Scheduled} cannot reference an arbitrary bean property; {@code every} only
* resolves a MicroProfile Config placeholder. The cleanup interval must therefore be exposed as
* a config key (e.g. {@code stirling.temp.cleanup-interval}) bound to the same value, and the
* minutes->duration mapping handled in config. Default below is 30m.
*/
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */
@Scheduled(every = "{stirling.temp.cleanup-interval:30m}")
public void scheduledCleanup() {
log.info("Running scheduled temporary file cleanup");
@@ -0,0 +1,168 @@
package stirling.software.common.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.enterprise.context.ApplicationScoped;
import lombok.RequiredArgsConstructor;
import stirling.software.common.model.tool.ToolDiagnostic;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIOSource;
import stirling.software.common.model.tool.ToolIOSpec;
/**
* Whether a chain of steps can run: what each produces against what the next accepts.
*
* <p>The frontend and the AI engine implement the same rules against their generated copies, so a
* chain can be checked without a round trip. {@code testing/tool-io-cases.json} pins all three to
* the same answers.
*/
@ApplicationScoped
@RequiredArgsConstructor
public class ToolChainValidator {
/** {@code parameters} may be null; only used to resolve an output that depends on one. */
public record Step(String operation, Map<String, Object> parameters) {}
private final ToolIOSource toolIO;
public List<ToolDiagnostic> validate(List<Step> steps) {
return validate(steps, null);
}
/**
* @param sourceFormat the format entering step one, or null when unknown
*/
public List<ToolDiagnostic> validate(List<Step> steps, ToolFormat sourceFormat) {
List<ToolDiagnostic> diagnostics = new ArrayList<>();
ToolIOSpec.Output carried = null;
for (int i = 0; i < steps.size(); i++) {
Step step = steps.get(i);
Optional<ToolIOSpec> found = toolIO.find(step.operation());
if (found.isEmpty()) {
diagnostics.add(
ToolDiagnostic.warn(
i,
ToolDiagnostic.UNDECLARED,
"Step "
+ step.operation()
+ " does not declare what it accepts or produces, so the"
+ " rest of the chain cannot be checked."));
// Nothing is known past an undeclared step.
carried = null;
continue;
}
ToolIOSpec spec = found.get();
// Only the first step is handed the pipeline's input. Every later step is handed the
// previous step's output, which is simply unknown once an undeclared step intervened -
// checking it against the input again would judge it on a format it never receives.
if (i == 0) {
checkSource(diagnostics, i, step, spec, sourceFormat);
} else if (carried != null) {
checkTransition(diagnostics, i, step, spec, carried);
}
carried = spec.resolveOutput(step.parameters());
}
return diagnostics;
}
public static boolean hasErrors(List<ToolDiagnostic> diagnostics) {
return diagnostics.stream().anyMatch(d -> d.severity() == ToolDiagnostic.Severity.ERROR);
}
private static void checkSource(
List<ToolDiagnostic> diagnostics,
int index,
Step step,
ToolIOSpec spec,
ToolFormat sourceFormat) {
if (sourceFormat == null || spec.acceptsFormat(sourceFormat)) {
return;
}
diagnostics.add(
ToolDiagnostic.error(
index,
ToolDiagnostic.SOURCE_MISMATCH,
"Step "
+ step.operation()
+ " accepts "
+ describe(spec)
+ " but the pipeline's input is "
+ sourceFormat
+ "."));
}
private static void checkTransition(
List<ToolDiagnostic> diagnostics,
int index,
Step step,
ToolIOSpec spec,
ToolIOSpec.Output previous) {
if (previous.format() == ToolFormat.NONE) {
diagnostics.add(
ToolDiagnostic.error(
index,
ToolDiagnostic.FORMAT_MISMATCH,
"The previous step returns a report rather than a file, so "
+ step.operation()
+ " has nothing to run on."));
return;
}
if (!spec.acceptsFormat(previous.format())) {
String message =
"Step "
+ step.operation()
+ " accepts "
+ describe(spec)
+ " but the previous step produces "
+ previous.format()
+ ".";
diagnostics.add(
previous.certain()
? ToolDiagnostic.error(index, ToolDiagnostic.FORMAT_MISMATCH, message)
// Unresolved output: may yet be fine once the step is configured.
: ToolDiagnostic.warn(index, ToolDiagnostic.OUTPUT_UNCERTAIN, message));
return;
}
if (!previous.certain()) {
diagnostics.add(
ToolDiagnostic.warn(
index,
ToolDiagnostic.OUTPUT_UNCERTAIN,
"The previous step's output depends on how it is configured, so this"
+ " step may not be able to run."));
return;
}
if (previous.arity().isMultiOutput()) {
diagnostics.add(
spec.arity().isMultiInput()
? ToolDiagnostic.info(
index,
ToolDiagnostic.FAN_IN,
"This step combines every file the previous step produced.")
: ToolDiagnostic.info(
index,
ToolDiagnostic.FAN_OUT,
"This step runs once for each file the previous step"
+ " produced."));
}
}
private static String describe(ToolIOSpec spec) {
return spec.accepts().stream()
.map(Enum::name)
.sorted()
.reduce((a, b) -> a + " or " + b)
.orElse("nothing");
}
}
@@ -0,0 +1,178 @@
package stirling.software.common.service;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.model.tool.ToolIOSource;
import stirling.software.common.model.tool.ToolIOSpec;
/**
* Reads every {@link ToolIO} declaration off its handler method at startup and serves it by
* endpoint path. Replaces parsing the same information out of the description prose, which meant
* fetching our own {@code /v1/api-docs} over HTTP first.
*
* <p>MIGRATION (Spring -> Quarkus): discovery previously enumerated Spring MVC's {@code
* RequestMappingHandlerMapping} beans and read each {@code RequestMappingInfo}'s direct paths.
* Quarkus/RESTEasy Reactive has no runtime handler-mapping registry, so the paths are rebuilt from
* the JAX-RS annotations on the CDI beans instead: the resource class's {@code @Path} joined with
* the method's {@code @Path} (falling back to {@link AutoJobPostMapping#value()} for a method that
* only declares its route there).
*/
@Slf4j
@ApplicationScoped
public class ToolIORegistry implements ToolMetadataService, ToolIOSource {
private final BeanManager beanManager;
// Written on the startup thread, read on request threads. The container's lifecycle
// establishes happens-before, so no volatile (same as AiEngineEndpointResolver).
private Map<String, ToolIOSpec> specsByPath = Map.of();
private boolean discovered = false;
// Keep this the only constructor: with two, Arc cannot pick an injection point.
@Inject
public ToolIORegistry(BeanManager beanManager) {
this.beanManager = beanManager;
}
/** A registry over known declarations rather than ones discovered from the container. */
static ToolIORegistry forSpecs(Map<String, ToolIOSpec> specs) {
ToolIORegistry registry = new ToolIORegistry(null);
registry.specsByPath = Map.copyOf(specs);
registry.discovered = true;
return registry;
}
void onStart(@Observes StartupEvent event) {
discoverToolIO();
}
/**
* Idempotent, and also called lazily on first read: the OpenAPI filter that publishes these
* declarations runs at its own point in startup, which is not ordered against this observer.
*/
public synchronized void discoverToolIO() {
if (discovered || beanManager == null) {
return;
}
Map<String, ToolIOSpec> specs = new TreeMap<>();
for (Bean<?> bean : beanManager.getBeans(Object.class, Any.Literal.INSTANCE)) {
register(specs, bean.getBeanClass());
}
specsByPath = Map.copyOf(specs);
discovered = true;
log.debug("Discovered {} endpoints declaring @ToolIO", specsByPath.size());
}
private static void register(Map<String, ToolIOSpec> target, Class<?> resourceClass) {
jakarta.ws.rs.Path classPath = resourceClass.getAnnotation(jakarta.ws.rs.Path.class);
if (classPath == null) {
return;
}
for (Method method : resourceClass.getMethods()) {
ToolIO annotation = method.getAnnotation(ToolIO.class);
if (annotation == null) {
continue;
}
ToolIOSpec spec = ToolIOSpec.from(annotation);
for (String pattern : extractPatterns(classPath, method)) {
target.put(pattern, spec);
}
}
}
@Override
public Optional<ToolIOSpec> find(String operationPath) {
if (!discovered) {
discoverToolIO();
}
return Optional.ofNullable(specsByPath.get(operationPath));
}
@Override
public boolean isMultiInput(String operationPath) {
return find(operationPath).map(spec -> spec.arity().isMultiInput()).orElse(false);
}
@Override
public List<String> getExtensionTypes(boolean output, String operationPath) {
Optional<ToolIOSpec> spec = find(operationPath);
if (spec.isEmpty()) {
return null;
}
List<String> extensions =
output
? spec.get().resolveOutput().format().getExtensions()
: spec.get().acceptedExtensions();
// Callers express "no restriction" as null.
return extensions.isEmpty() ? null : extensions;
}
@Override
public boolean shouldUnpackZipResponse(String operationPath) {
// Multi-output zips purely as transport. A single-output ZIP is the deliverable
// (extract-attachments) and stays packed.
return find(operationPath)
.map(spec -> spec.resolveOutput().arity().isMultiOutput())
.orElse(false);
}
private static Set<String> extractPatterns(jakarta.ws.rs.Path classPath, Method handlerMethod) {
Set<String> patterns = new LinkedHashSet<>();
jakarta.ws.rs.Path methodPath = handlerMethod.getAnnotation(jakarta.ws.rs.Path.class);
if (methodPath != null) {
patterns.add(join(classPath.value(), methodPath.value()));
return patterns;
}
// Routing lives on @AutoJobPostMapping for endpoints that never got their own @Path.
AutoJobPostMapping autoJob = handlerMethod.getAnnotation(AutoJobPostMapping.class);
if (autoJob != null) {
for (String value : autoJob.value()) {
patterns.add(join(classPath.value(), value));
}
}
return patterns;
}
private static String join(String base, String suffix) {
String head = normalise(base);
String tail = normalise(suffix);
if (tail.isEmpty()) {
return head;
}
return head.isEmpty() ? tail : head + tail;
}
/** Leading slash, no trailing one, so the two halves concatenate cleanly. */
private static String normalise(String path) {
if (path == null || path.isBlank() || "/".equals(path)) {
return "";
}
String trimmed = path.trim();
if (!trimmed.startsWith("/")) {
trimmed = "/" + trimmed;
}
while (trimmed.length() > 1 && trimmed.endsWith("/")) {
trimmed = trimmed.substring(0, trimmed.length() - 1);
}
return trimmed;
}
}
@@ -17,12 +17,12 @@ public interface ToolMetadataService {
List<String> getExtensionTypes(boolean output, String operationPath);
/**
* Returns true when the endpoint's ZIP response is a transport for multiple typed results and
* should be unpacked: multi-output endpoints (Type:SIMO / Type:MIMO) and wrapper declarations
* such as {@code Output:ZIP-PDF} or {@code Output:IMAGE/ZIP}.
* Returns true when the endpoint's ZIP response is a transport for several results and should
* be unpacked, which is exactly the multi-output endpoints (a {@code SIMO} or {@code MIMO}
* arity).
*
* <p>Returns false for a bare {@code Output:ZIP} (e.g. {@code get-attachments}), where the
* archive itself is the deliverable and should be kept packed.
* <p>Returns false for an endpoint whose declared output is an archive in its own right (for
* example {@code extract-attachments}), where unpacking would discard the deliverable.
*/
boolean shouldUnpackZipResponse(String operationPath);
}
@@ -7,9 +7,6 @@ import java.util.Map;
public class ErrorUtils {
// TODO: Migration required - server-rendered error view removed; surface via JAX-RS
// ExceptionMapper. Spring MVC org.springframework.ui.Model has no Quarkus/Jakarta (JAX-RS)
// drop-in; the method now mutates and returns a plain Map<String, Object> model holder.
public static Map<String, Object> exceptionToModel(Map<String, Object> model, Exception ex) {
StringWriter sw = new StringWriter();
ex.printStackTrace(new PrintWriter(sw));
@@ -20,11 +17,6 @@ public class ErrorUtils {
return model;
}
// TODO: Migration required - server-rendered error view removed; surface via JAX-RS
// ExceptionMapper. Spring MVC org.springframework.web.servlet.ModelAndView has no
// Quarkus/Jakarta (JAX-RS) drop-in; the method now returns a plain Map<String, Object> model
// holder instead of a ModelAndView (the incoming model parameter is retained for signature
// compatibility but is no longer the Spring Model type).
public static Map<String, Object> exceptionToModelView(
Map<String, Object> model, Exception ex) {
StringWriter sw = new StringWriter();
@@ -254,10 +254,6 @@ public class GeneralUtils {
* {@code file:} patterns are resolved with {@link java.nio.file.Files#list}; {@code classpath:}
* patterns are resolved via the classloader and only support directory resources that live on
* the filesystem.
*
* <p>TODO: Migration required - {@code classpath:} resolution does not enumerate entries inside
* a packaged JAR. For uber-jar deployments, prefer serving these assets from {@code
* META-INF/resources/} or build a Jandex/build-time index of the matching files.
*/
public static Resource[] getResourcesFromLocationPattern(String locationPattern)
throws Exception {
@@ -537,10 +537,6 @@ public final class RegexPatternUtils {
getPattern("[/\\\\?%*:|\"<>]"); // Unsafe filename characters
getPattern("[^a-zA-Z0-9 ]"); // Input sanitization
getPattern("[^a-zA-Z0-9]"); // Filename sanitization
// API doc patterns
getPattern("Output:\\s*(\\w+)");
getPattern("Input:\\s*(\\w+)");
getPattern("Type:\\s*(\\w+)");
log.debug("Pre-compiled {} common regex patterns", patternCache.size());
}
@@ -550,23 +546,6 @@ public final class RegexPatternUtils {
"^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*(?:\\.[A-Za-z]{2,})$");
}
/* Pattern for matching Output:<TYPE> in API descriptions */
public Pattern getApiDocOutputTypePattern() {
return getPattern("Output:\\s*(\\w+)");
}
/* Pattern for matching Input:<TYPE> in API descriptions */
public Pattern getApiDocInputTypePattern() {
return getPattern("Input:\\s*(\\w+)");
}
/**
* Pattern for matching Type:<CODE> in API descriptions
*/
public Pattern getApiDocTypePattern() {
return getPattern("Type:\\s*(\\w+)");
}
/* Pattern for validating file extensions (2-4 alphanumeric, case-insensitive) */
public Pattern getFileExtensionValidationPattern() {
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
@@ -63,9 +63,6 @@ public class SpringContextHolder {
}
try {
// TODO: Migration required - Spring looked up by bean name across all types; here we
// resolve a @Named CDI bean of Object.class. Verify named beans are registered with a
// matching @jakarta.inject.Named qualifier so this lookup resolves the intended bean.
Instance<Object> instance = container.select(Object.class, NamedLiteral.of(beanName));
if (!instance.isResolvable()) {
log.error("Error getting bean '{}': bean is not resolvable", beanName);
@@ -27,11 +27,6 @@ import stirling.software.common.model.api.misc.HighContrastColorCombination;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import stirling.software.common.model.io.InputStreamResource;
// TODO: Migration required - MultipartFile is the constructor parameter type that must match
// the parent ReplaceAndInvertColorStrategy(MultipartFile, ReplaceAndInvert) constructor (not in
// scope for this migration). There is no JAX-RS drop-in for this widely used public signature;
// retained until the parent and its callers are migrated together.
@Slf4j
public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
@@ -17,7 +17,7 @@ import stirling.software.common.model.ApplicationProperties;
* cluster mode is off or {@code backplane=inprocess}, and are skipped when {@code
* backplane=valkey}.
*/
@Disabled("TODO: Migration required - Spring Boot test framework not available in Quarkus")
@Disabled("Spring Boot test framework not available in Quarkus")
class InProcessConfigurationConditionalTest {
private final ApplicationContextRunner runner =
@@ -15,7 +15,7 @@ import org.springframework.core.env.StandardEnvironment;
import stirling.software.common.configuration.InstallationPathConfig;
@Disabled("TODO: Migration required - Spring Boot test framework not available in Quarkus")
@Disabled("Spring Boot test framework not available in Quarkus")
class ApplicationPropertiesDynamicYamlPropertySourceTest {
@Test
@@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test;
import stirling.software.common.model.io.Resource;
@Disabled("TODO: Migration required - Spring Boot test framework not available in Quarkus")
@Disabled("Spring Boot test framework not available in Quarkus")
class ApplicationPropertiesSaml2ResourceTest {
@Test
@@ -17,13 +17,13 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.cluster.inprocess.LocalDiskFileStore;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.io.FileSystemResource;
import stirling.software.common.service.FileStorage.StoredFile;
class FileStorageMoreTest {
@@ -33,9 +33,9 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.job.JobResponse;
import stirling.software.common.util.ExceptionUtils;
@@ -15,8 +15,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.service.MobileScannerService.FileMetadata;
import stirling.software.common.service.MobileScannerService.SessionInfo;
@@ -0,0 +1,159 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolDiagnostic;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIOSource;
import stirling.software.common.model.tool.ToolIOSpec;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;
/** The shared cases in {@code testing/tool-io-cases.json}, which all three implementations run. */
class ToolChainValidatorConformanceTest {
private static final JsonMapper MAPPER = JsonMapper.builder().build();
@TestFactory
Stream<DynamicTest> sharedCases() throws IOException {
JsonNode root = MAPPER.readTree(Files.readString(casesFile()));
Map<String, ToolIOSpec> specs = readSpecs(root.get("specs"));
List<DynamicTest> tests = new ArrayList<>();
for (JsonNode testCase : root.get("cases")) {
tests.add(
DynamicTest.dynamicTest(
testCase.get("name").asString(), () -> runCase(testCase, specs)));
}
return tests.stream();
}
private static void runCase(JsonNode testCase, Map<String, ToolIOSpec> specs) {
Map<String, ToolIOSpec> registry = new HashMap<>();
List<ToolChainValidator.Step> steps = new ArrayList<>();
int index = 0;
for (JsonNode stepNode : testCase.get("steps")) {
// Each step gets its own path so the same spec can appear twice in a chain.
String operation = "/op/" + index++;
JsonNode specName = stepNode.get("spec");
if (specName != null && !specName.isNull()) {
registry.put(operation, specs.get(specName.asString()));
}
steps.add(new ToolChainValidator.Step(operation, readParameters(stepNode)));
}
JsonNode sourceNode = testCase.get("sourceFormat");
ToolFormat sourceFormat =
sourceNode == null || sourceNode.isNull()
? null
: ToolFormat.valueOf(sourceNode.asString());
List<ToolDiagnostic> actual =
new ToolChainValidator(ToolIOSource.of(registry)).validate(steps, sourceFormat);
assertEquals(summarise(testCase.get("expected")), summarise(actual), describe(actual));
}
private static Map<String, Object> readParameters(JsonNode stepNode) {
JsonNode parameters = stepNode.get("parameters");
if (parameters == null || parameters.isNull()) {
return null;
}
Map<String, Object> values = new HashMap<>();
parameters.propertyStream().forEach(e -> values.put(e.getKey(), e.getValue().asString()));
return values;
}
private static Map<String, ToolIOSpec> readSpecs(JsonNode node) {
Map<String, ToolIOSpec> specs = new HashMap<>();
node.propertyStream()
.forEach(entry -> specs.put(entry.getKey(), readSpec(entry.getValue())));
return specs;
}
private static ToolIOSpec readSpec(JsonNode node) {
Set<ToolFormat> accepts = new LinkedHashSet<>();
for (JsonNode format : node.get("accepts")) {
accepts.add(ToolFormat.valueOf(format.asString()));
}
List<ToolIOSpec.Case> cases = new ArrayList<>();
for (JsonNode rule : node.get("cases")) {
List<ToolIOSpec.When> when = new ArrayList<>();
for (JsonNode condition : rule.get("when")) {
List<String> matches = new ArrayList<>();
for (JsonNode match : condition.get("matches")) {
matches.add(match.asString());
}
when.add(new ToolIOSpec.When(condition.get("param").asString(), matches));
}
cases.add(
new ToolIOSpec.Case(
when,
ToolFormat.valueOf(rule.get("produces").asString()),
ToolArity.valueOf(rule.get("arity").asString())));
}
return new ToolIOSpec(
accepts,
ToolFormat.valueOf(node.get("produces").asString()),
ToolArity.valueOf(node.get("arity").asString()),
cases);
}
/** Messages are free text, so compare only the contractual parts. */
private static List<String> summarise(List<ToolDiagnostic> diagnostics) {
return diagnostics.stream()
.map(d -> d.stepIndex() + ":" + d.severity() + ":" + d.code())
.toList();
}
private static List<String> summarise(JsonNode expected) {
List<String> summary = new ArrayList<>();
for (JsonNode node : expected) {
summary.add(
node.get("stepIndex").asInt()
+ ":"
+ node.get("severity").asString()
+ ":"
+ node.get("code").asString());
}
return summary;
}
private static String describe(List<ToolDiagnostic> actual) {
return actual.stream()
.map(ToolDiagnostic::message)
.reduce((a, b) -> a + " | " + b)
.orElse("no diagnostics");
}
/** Shared with the frontend and engine, so it lives at the repo root. */
private static Path casesFile() {
Path current = Path.of("").toAbsolutePath();
while (current != null) {
Path candidate = current.resolve("testing/tool-io-cases.json");
if (Files.exists(candidate)) {
return candidate;
}
current = current.getParent();
}
throw new IllegalStateException(
"testing/tool-io-cases.json not found above the working directory");
}
}
@@ -0,0 +1,119 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.model.tool.ToolIOCase;
import stirling.software.common.model.tool.ToolIOSpec;
import stirling.software.common.model.tool.ToolIOWhen;
/**
* The registry reads real {@code @ToolIO} annotations off real handler mappings. Everything else
* checks the logic against a hand-built map, which would still pass if discovery silently found
* nothing.
*/
class ToolIODiscoveryTest {
@RestController
@RequestMapping("/api/v1/fixture")
static class FixtureController {
@PostMapping("/rotate")
@ToolIO(produces = ToolFormat.PDF)
public String rotate() {
return "";
}
@PostMapping("/split")
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
public String split() {
return "";
}
@PostMapping("/add-password")
@ToolIO(
produces = ToolFormat.PDF_ENCRYPTED,
cases =
@ToolIOCase(
when = {
@ToolIOWhen(param = "password", matches = ""),
@ToolIOWhen(param = "ownerPassword", matches = "")
},
produces = ToolFormat.PDF,
arity = ToolArity.SISO))
public String addPassword() {
return "";
}
@PostMapping("/undeclared")
public String undeclared() {
return "";
}
}
private static ToolIORegistry registry;
@BeforeAll
static void discover() {
StaticWebApplicationContext context = new StaticWebApplicationContext();
context.registerSingleton("fixtureController", FixtureController.class);
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setApplicationContext(context);
mapping.afterPropertiesSet();
context.getBeanFactory().registerSingleton("requestMappingHandlerMapping", mapping);
registry = new ToolIORegistry(context);
registry.discoverToolIO();
}
@Test
void readsDeclarationsOffHandlerMethods() {
ToolIOSpec rotate = registry.find("/api/v1/fixture/rotate").orElseThrow();
assertEquals(ToolFormat.PDF, rotate.produces());
assertEquals(ToolArity.SISO, rotate.arity());
assertTrue(rotate.acceptsFormat(ToolFormat.PDF));
assertFalse(rotate.acceptsFormat(ToolFormat.PDF_ENCRYPTED));
}
@Test
void skipsMethodsWithNoDeclaration() {
assertTrue(registry.find("/api/v1/fixture/undeclared").isEmpty());
}
@Test
void carriesArityThroughToTheUnpackDecision() {
assertTrue(registry.shouldUnpackZipResponse("/api/v1/fixture/split"));
assertFalse(registry.shouldUnpackZipResponse("/api/v1/fixture/rotate"));
}
@Test
void carriesCasesThroughToOutputResolution() {
ToolIOSpec spec = registry.find("/api/v1/fixture/add-password").orElseThrow();
assertEquals(
ToolFormat.PDF_ENCRYPTED,
spec.resolveOutput(Map.of("password", "x", "ownerPassword", "")).format());
assertEquals(
ToolFormat.PDF,
spec.resolveOutput(Map.of("password", "", "ownerPassword", "")).format());
}
@Test
void exposesInputExtensionsForRunTimeFileChecks() {
assertEquals(List.of("pdf"), registry.getExtensionTypes(false, "/api/v1/fixture/rotate"));
}
}
@@ -0,0 +1,87 @@
package stirling.software.common.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIOSpec;
/** The {@link ToolMetadataService} behaviour the pipeline executors depend on. */
class ToolIORegistryTest {
private static final String SPLIT = "/api/v1/general/split-pages";
private static final String MERGE = "/api/v1/general/merge-pdfs";
private static final String ROTATE = "/api/v1/general/rotate-pdf";
private static final String ATTACHMENTS = "/api/v1/security/get-attachments";
private static final String EXTRACT_IMAGES = "/api/v1/misc/extract-images";
private static final String CONVERT_ANY = "/api/v1/convert/file/pdf";
private static final String UNKNOWN = "/api/v1/general/does-not-exist";
private static ToolIOSpec spec(ToolFormat accepts, ToolFormat produces, ToolArity arity) {
return new ToolIOSpec(Set.of(accepts), produces, arity, List.of());
}
private final ToolIORegistry registry =
ToolIORegistry.forSpecs(
Map.of(
SPLIT, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.SIMO),
MERGE, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.MISO),
ROTATE, spec(ToolFormat.PDF, ToolFormat.PDF, ToolArity.SISO),
ATTACHMENTS, spec(ToolFormat.PDF, ToolFormat.ZIP, ToolArity.SISO),
EXTRACT_IMAGES, spec(ToolFormat.PDF, ToolFormat.IMAGE, ToolArity.SIMO),
CONVERT_ANY, spec(ToolFormat.ANY, ToolFormat.PDF, ToolArity.SISO)));
@Test
void multiInputFollowsArity() {
assertTrue(registry.isMultiInput(MERGE));
assertFalse(registry.isMultiInput(SPLIT));
assertFalse(registry.isMultiInput(ROTATE));
assertFalse(registry.isMultiInput(UNKNOWN));
}
@Test
void inputExtensionsComeFromAcceptedFormats() {
assertEquals(List.of("pdf"), registry.getExtensionTypes(false, ROTATE));
}
@Test
void outputExtensionsComeFromTheProducedFormat() {
assertEquals(List.of("pdf"), registry.getExtensionTypes(true, ROTATE));
assertTrue(registry.getExtensionTypes(true, EXTRACT_IMAGES).contains("png"));
}
@Test
void noRestrictionIsReportedAsNull() {
// Callers treat null as "any type accepted".
assertNull(registry.getExtensionTypes(false, CONVERT_ANY));
assertNull(registry.getExtensionTypes(false, UNKNOWN));
}
@Test
void multiOutputResponsesAreUnpacked() {
assertTrue(registry.shouldUnpackZipResponse(SPLIT));
assertTrue(registry.shouldUnpackZipResponse(EXTRACT_IMAGES));
}
@Test
void anArchiveDeliverableStaysPacked() {
// The archive is the deliverable; unpacking would lose it.
assertFalse(registry.shouldUnpackZipResponse(ATTACHMENTS));
}
@Test
void singleOutputResponsesAreNotUnpacked() {
assertFalse(registry.shouldUnpackZipResponse(ROTATE));
assertFalse(registry.shouldUnpackZipResponse(MERGE));
assertFalse(registry.shouldUnpackZipResponse(UNKNOWN));
}
}
@@ -13,9 +13,9 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
@@ -25,9 +25,9 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
@@ -15,9 +15,10 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.io.Resource;
/**
* Gap-coverage tests for {@link GeneralUtils}. Targets the public methods NOT already exercised by
@@ -26,8 +26,8 @@ import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
@@ -27,8 +27,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
/**
@@ -34,9 +34,9 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
/**
@@ -325,11 +325,8 @@ class RegexPatternUtilsMoreTest {
}
@Test
void pageModeAndApiDocPatterns() {
void pageModePattern() {
assertTrue(utils.getPageModePattern().matcher("a/b").find());
assertTrue(utils.getApiDocOutputTypePattern().matcher("Output: PDF").find());
assertTrue(utils.getApiDocInputTypePattern().matcher("Input: PDF").find());
assertTrue(utils.getApiDocTypePattern().matcher("Type: WEB").find());
}
@Test
@@ -20,9 +20,9 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.io.Resource;
/**
* Tests for {@link ZipExtractionUtils} that build real in-memory ZIP byte streams and exercise
@@ -17,12 +17,12 @@ import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.InputStreamResource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.api.misc.HighContrastColorCombination;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import stirling.software.common.model.io.InputStreamResource;
/**
* Gap-filling tests for {@link CustomColorReplaceStrategy#replace()} that run the full restyle loop
+1 -1
View File
@@ -60,7 +60,7 @@ dependencies {
// CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7)
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.5'
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation 'org.apache.poi:poi-ooxml:5.5.1'
@@ -67,20 +67,6 @@ public class SPDFApplication implements QuarkusApplication {
customSettingsPath.toString());
}
// TODO: Migration required - the Spring "spring.config.additional-location" property used
// to
// load the external settings/customSettings YAML files into the environment. Quarkus uses
// SmallRye Config; wire these files via a config source instead, e.g. set the system
// property
// "smallrye.config.locations" to the (comma-separated) file: URLs before this point, or
// register a custom ConfigSourceFactory. The directories/log lines above are preserved.
// TODO: Migration required - profile auto-detection (former getActiveProfile / Spring
// setAdditionalProfiles) must be expressed via "quarkus.profile". The classpath-shape
// detection logic is retained below in getActiveProfile(); translate its result into the
// "quarkus.profile" system property (e.g. System.setProperty("quarkus.profile", ...))
// before
// Quarkus.run if profile-based config layering is required.
getActiveProfile(args);
Quarkus.run(SPDFApplication.class, args);
@@ -168,10 +154,6 @@ public class SPDFApplication implements QuarkusApplication {
// Former @EventListener(ApplicationReadyEvent) onApplicationReady().
private void onApplicationReady() {
// TODO: Migration required - the Spring "local.server.port" property exposed the actual
// runtime port (relevant for server.port=0 / "auto" port assignment). In Quarkus read
// the resolved port from config "quarkus.http.port" (or observe an HTTP-started event)
// and update serverPortStatic here. Falling back to the configured value for now.
String port = config.getOptionalValue("quarkus.http.port", String.class).orElse(null);
if (port != null) {
serverPortStatic = port;
@@ -28,8 +28,6 @@ class AppUpdateService {
// ("Producer method for a normal scoped bean must not have a primitive type"). @Dependent
// recomputes the value at each injection point, the closest behaviour to per-request
// evaluation.
// TODO: Migration required - if true per-HTTP-request semantics are needed, wrap the value in a
// @RequestScoped holder object instead of producing a bare boolean.
@Produces
@Named("shouldShow")
@Dependent
@@ -29,17 +29,6 @@ public class EndpointInspector {
private void discoverEndpoints() {
try {
// TODO: Migration required - this previously used Spring MVC's
// RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.*) to
// enumerate all registered GET handler mappings via the ApplicationContext at
// ContextRefreshedEvent. Quarkus/JAX-RS (RESTEasy Reactive) has no equivalent
// runtime-queryable handler-mapping registry. Options for porting:
// - Build-time scan of @jakarta.ws.rs.Path + @jakarta.ws.rs.GET via a Quarkus
// build step / Jandex index, or
// - Query the OpenAPI model (quarkus-smallrye-openapi) for GET paths, or
// - Maintain an explicit allow-list.
// Until one of the above is implemented, no endpoints are discovered and we fall
// back to the common wildcard endpoints below (preserving prior fallback behavior).
if (validGetEndpoints.isEmpty()) {
log.warn("No endpoints discovered. Adding common endpoints as fallback.");
@@ -18,12 +18,6 @@ import stirling.software.common.model.io.ClassPathResource;
import stirling.software.common.model.io.Resource;
import stirling.software.common.util.GeneralUtils;
// TODO: Migration required - Spring @Order(Ordered.HIGHEST_PRECEDENCE + 1) controlled the relative
// order of this startup hook against other initializers. CDI StartupEvent observers have no
// portable
// total ordering; if a specific run-before/run-after relationship is required, use @Priority on the
// observer parameter or @Observes(during=...) and coordinate ordering across the migrated startup
// beans.
@ApplicationScoped
@Slf4j
@RequiredArgsConstructor
@@ -8,16 +8,6 @@ import lombok.RequiredArgsConstructor;
import stirling.software.common.model.ApplicationProperties;
// TODO: Migration required - this class was a Spring MVC WebMvcConfigurer. Quarkus/JAX-RS has no
// WebMvcConfigurer, InterceptorRegistry, LocaleChangeInterceptor or SessionLocaleResolver.
// The locale-resolution logic (computing the default Locale from configuration) is preserved below
// as a CDI-produced Locale. The two pieces of behavior that previously came from the MVC machinery
// still need to be wired up by collaborators:
// 1. The "lang" request-param locale switching (old LocaleChangeInterceptor) must be implemented
// as a jakarta.ws.rs.container.ContainerRequestFilter that reads the "lang" query/form param
// and applies it for the request scope.
// 2. CleanUrlInterceptor (its own assigned file) must be converted to a ContainerRequestFilter
// and registered automatically via @Provider; it no longer needs explicit registration here.
@ApplicationScoped
@RequiredArgsConstructor
public class LocaleConfiguration {
@@ -34,10 +34,6 @@ import stirling.software.common.model.ApplicationProperties;
* <li>the {@code apiKey} security scheme + requirement when login is enabled;
* <li>the {@code PDFFile} {@code oneOf} (upload vs. server-side file id) schema.
* </ul>
*
* <p>TODO: Migration required - register this filter by setting {@code mp.openapi.filter=
* stirling.software.SPDF.config.OpenApiConfig} in application.properties (collaborator edit; not
* the assigned file). Without that key smallrye-openapi will not invoke this filter.
*/
public class OpenApiConfig implements OASFilter {
@@ -1,50 +1,5 @@
package stirling.software.SPDF.config;
// TODO: Migration required - springdoc's GroupedOpenApi (multiple OpenAPI documents
// grouped by path-matching) has NO direct equivalent in quarkus-smallrye-openapi, which
// serves a single document built automatically from @Tag/@Operation/JAX-RS annotations.
// The three groups below (file-processing "/api/v1/**" minus management/system paths,
// management "/api/v1/admin/**" etc., and system "/api/v1/ui-data/**" etc.) plus the
// pdfFileOneOfCustomizer (@Qualifier("pdfFileOneOfCustomizer") OpenApiCustomizer) need to
// be re-expressed. Options:
// 1. Implement org.eclipse.microprofile.openapi.OASFilter providers (registered via
// mp.openapi.filter or @Provider) for the per-document info()/title/description and for
// the pdfFileOneOf customization. A single smallrye document cannot be split per-path
// into 3 named groups, so the grouping/displayName/pathsToMatch/pathsToExclude behavior
// is lost unless multiple smallrye-openapi profiles/configs are introduced.
// 2. Set the single top-level title/description via application.properties
// (quarkus.smallrye-openapi.info-title / info-description) and drop grouping.
// Preserving the original group metadata here as reference until the OASFilter(s) are written.
//
// Original groups (springdoc):
// group "file-processing" (displayName "File Processing"):
// pathsToMatch: /api/v1/**
// pathsToExclude: /api/v1/admin/**, /api/v1/user/**, /api/v1/settings/**, /api/v1/team/**,
// /api/v1/auth/**, /api/v1/invite/**, /api/v1/audit/**, /api/v1/ui-data/**,
// /api/v1/proprietary/ui-data/**, /api/v1/info/**, /api/v1/general/job/**,
// /api/v1/general/files/**, /api/v1/general/signatures/**, /api/v1/database/**,
// /api/v1/storage/**, /api/v1/proprietary/signatures/**, /api/v1/workflow/participant/**,
// /api/v1/security/cert-sign/sessions, /api/v1/security/cert-sign/sessions/**,
// /api/v1/security/cert-sign/sign-requests, /api/v1/security/cert-sign/sign-requests/**,
// /api/v1/security/cert-sign/validate-certificate
// customizers: pdfFileOneOfCustomizer; info.title "Stirling PDF - Processing API",
// description "APIs for converting, editing, securing, and analysing PDF documents. Use
// these endpoints to automate common PDF tasks (like split, merge, convert, OCR) and plug
// them into your own apps and backend jobs."
// group "management" (displayName "Management"):
// pathsToMatch: /api/v1/admin/**, /api/v1/user/**, /api/v1/settings/**, /api/v1/team/**,
// /api/v1/auth/**, /api/v1/invite/**, /api/v1/audit/**, /api/v1/database/**,
// /api/v1/storage/**, /api/v1/proprietary/signatures/**, /api/v1/workflow/participant/**,
// /api/v1/security/cert-sign/sessions, /api/v1/security/cert-sign/sessions/**,
// /api/v1/security/cert-sign/sign-requests, /api/v1/security/cert-sign/sign-requests/**,
// /api/v1/security/cert-sign/validate-certificate
// info.title "Stirling PDF - Management API", description "Endpoints for authentication,
// user management, invitations, audit logging, and system configuration."
// group "system" (displayName "System & UI API"):
// pathsToMatch: /api/v1/ui-data/**, /api/v1/proprietary/ui-data/**, /api/v1/info/**,
// /api/v1/general/job/**, /api/v1/general/files/**, /api/v1/general/signatures/**
// info.title "Stirling PDF - System API", description "System information, UI metadata,
// job status, and file management endpoints."
public class SpringDocConfig {
// All springdoc GroupedOpenApi @Bean producers removed; smallrye-openapi builds the
// document from annotations. See the TODO above for how to restore grouping/customizers.
@@ -18,11 +18,6 @@ import stirling.software.SPDF.service.WeeklyActiveUsersService;
* Filter to track browser IDs for Weekly Active Users (WAU) counting. Only active when security is
* disabled (no-login mode).
*/
// TODO: Migration required - Spring @ConditionalOnProperty(name="security.enableLogin",
// havingValue="false") had no direct CDI equivalent for conditional bean registration. The filter
// is now always registered (@Provider) and the condition is enforced at request time by reading the
// 'security.enableLogin' config property below. Verify the property key matches Quarkus config
// (originally bound from ApplicationProperties.security.enableLogin).
@Provider
@ApplicationScoped
@RequiredArgsConstructor
@@ -60,16 +60,6 @@ public class WebMvcConfig implements ContainerResponseFilter {
+ ", public, stale-while-revalidate="
+ Duration.ofDays(7).toSeconds();
// TODO: Migration required - in Spring, addResourceHandlers also registered the physical
// resource locations (InstallationPathConfig.getStaticPath() + "classpath:/static/") and an
// EncodedResourceResolver (gzip/brotli pre-compressed asset serving). In Quarkus, static
// file serving is handled by quarkus.http via configuration:
// quarkus.http.static-resources... and/or a Servlet/RouteFilter mapping
// InstallationPathConfig.getStaticPath() as an external static root.
// The EncodedResourceResolver behavior (serving *.gz/*.br variants) has no direct WebMvc
// equivalent; enable quarkus.http.enable-compression or pre-compressed static handling.
// This filter only reproduces the per-path Cache-Control headers below.
@Override
public void filter(
ContainerRequestContext requestContext, ContainerResponseContext responseContext)
@@ -149,17 +139,6 @@ public class WebMvcConfig implements ContainerResponseFilter {
|| path.equals("/manifest-classic.json");
}
// TODO: Migration required - Quarkus has built-in CORS handling via quarkus.http.cors.*
// config properties (quarkus.http.cors.origins, .methods, .headers, .exposed-headers,
// .access-control-allow-credentials, .access-control-max-age). However, the original logic is
// *dynamic* (Tauri-mode detection + ApplicationProperties-driven origins + always-on Tauri
// origins), which static config cannot express. The logic is preserved below and applied via
// this response filter. Note: a ContainerResponseFilter cannot short-circuit/answer the CORS
// preflight (OPTIONS) request the way Spring's CorsRegistry does; for full preflight handling,
// enable quarkus.http.cors=true and reconcile with these dynamic rules, or add a
// ContainerRequestFilter that handles OPTIONS. Reflecting the requesting Origin is used here
// since Access-Control-Allow-Origin does not support patterns/wildcards-with-credentials.
/**
* Reproduces the dynamic CORS configuration from the original {@code addCorsMappings}: Tauri
* mode, user-configured origins (always augmented with Tauri origins), or allow-all fallback.
@@ -62,7 +62,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get PDF page count",
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
description = "Returns total number of pages in PDF.")
public Response getPageCount(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -82,7 +82,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get basic PDF information",
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
description = "Returns page count, version, file size.")
public Response getBasicInfo(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -106,7 +106,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get PDF document properties",
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
description = "Returns title, author, subject, etc.")
public Response getDocumentProperties(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -144,7 +144,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get page dimensions for all pages",
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
description = "Returns width and height of each page.")
public Response getPageDimensions(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -173,8 +173,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get form field information",
description =
"Returns count and details of form fields. Input:PDF Output:JSON Type:SISO")
description = "Returns count and details of form fields.")
public Response getFormFields(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -206,7 +205,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get annotation information",
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
description = "Returns count and types of annotations.")
public Response getAnnotationInfo(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -240,8 +239,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get font information",
description =
"Returns list of fonts used in the document. Input:PDF Output:JSON Type:SISO")
description = "Returns list of fonts used in the document.")
public Response getFontInfo(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -275,8 +273,7 @@ public class AnalysisController {
@JsonDataResponse
@Operation(
summary = "Get security information",
description =
"Returns encryption and permission details. Input:PDF Output:JSON Type:SISO")
description = "Returns encryption and permission details.")
public Response getSecurityInfo(
@RestForm("fileInput") FileUpload fileInput, @RestForm("fileId") String fileId)
throws IOException {
@@ -32,6 +32,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
@@ -53,12 +55,13 @@ public class BookletImpositionController {
value = "/booklet-imposition",
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Create a booklet with proper page imposition",
description =
"This operation combines page reordering for booklet printing with multi-page layout. "
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
"This operation combines page reordering for booklet printing with multi-page"
+ " layout. It rearranges pages in the correct order for booklet printing and"
+ " places multiple pages on each sheet for proper folding and binding.")
public Response createBookletImposition(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -33,6 +33,8 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -142,11 +144,12 @@ public class CropController {
value = "/crop",
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.SMALL_WEIGHT)
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Crops a PDF document",
description =
"This operation takes an input PDF file and crops it according to the given"
+ " coordinates. Input:PDF Output:PDF Type:SISO")
+ " coordinates.")
public Response cropPdf(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -32,6 +32,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
@@ -58,6 +60,7 @@ public class EditTableOfContentsController {
value = "/extract-bookmarks",
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.SMALL_WEIGHT)
@ToolIO(produces = ToolFormat.JSON)
@Operation(
summary = "Extract PDF Bookmarks",
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
@@ -165,6 +168,7 @@ public class EditTableOfContentsController {
value = "/edit-table-of-contents",
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.SMALL_WEIGHT)
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Edit Table of Contents",
description = "Add or edit bookmarks/table of contents in a PDF document.")
@@ -39,6 +39,8 @@ import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.api.general.EditTextOperation;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
@@ -86,19 +88,18 @@ public class EditTextController {
value = "/edit-text",
resourceWeight = ResourceWeight.LARGE_WEIGHT)
@StandardPdfResponse
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Edit text in a PDF via find and replace",
description =
"Applies an ordered list of find/replace operations to the text in a PDF and"
+ " returns the edited PDF. Useful for find-and-replace, bulk renames"
+ " (e.g. updating a company name throughout a document), and copy"
+ " editing where the AI agent has identified specific replacements."
+ " Matching is performed against the joined text of each page, so"
+ " find strings can span multiple visual runs (titles split per word,"
+ " kerning-broken phrases). Cross-element matches are written as a"
+ " single replacement run anchored at the leftmost matched position;"
+ " centered or tracked text may shift left when its content changes."
+ " Input:PDF Output:PDF Type:SISO")
+ " returns the edited PDF. Useful for find-and-replace, bulk renames (e.g."
+ " updating a company name throughout a document), and copy editing where the AI"
+ " agent has identified specific replacements. Matching is performed against the"
+ " joined text of each page, so find strings can span multiple visual runs"
+ " (titles split per word, kerning-broken phrases). Cross-element matches are"
+ " written as a single replacement run anchored at the leftmost matched position;"
+ " centered or tracked text may shift left when its content changes.")
public Response editText(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -45,6 +45,9 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -274,12 +277,13 @@ public class MergeController {
@jakarta.ws.rs.Path("/merge-pdfs")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@StandardPdfResponse
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.MISO)
@Operation(
summary = "Merge multiple PDF files into one",
description =
"This endpoint merges multiple PDF files into a single PDF file. The merged"
+ " file will contain all pages from the input files in the order they were"
+ " provided. Input:PDF Output:PDF Type:MISO")
+ " provided.")
public Response mergePdfs(
@RestForm("fileInput") List<FileUpload> fileUploads,
@RestForm("sortType") String sortType,
@@ -31,6 +31,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralFormCopyUtils;
@@ -55,11 +57,12 @@ public class MultiPageLayoutController {
value = "/multi-page-layout",
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Merge multiple pages of a PDF document into a single page",
description =
"This operation takes an input PDF file and the number of pages to merge into a"
+ " single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
+ " single sheet in the output PDF file.")
public Response mergeMultiplePagesIntoOne(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -32,6 +32,9 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -56,11 +59,12 @@ public class PdfOverlayController {
@jakarta.ws.rs.Path("/overlay-pdfs")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@StandardPdfResponse
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.MISO)
@Operation(
summary = "Overlay PDF files in various modes",
description =
"Overlay PDF files onto a base PDF with different modes: Sequential,"
+ " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
+ " Interleaved, or Fixed Repeat.")
public Response overlayPdfs(
@RestForm("fileInput") FileUpload fileInput,
@RestForm("overlayFiles") List<FileUpload> overlayFileUploads,
@@ -36,6 +36,9 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -61,13 +64,13 @@ public class PosterPdfController {
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@MultiFileResponse
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
@Operation(
summary = "Split large PDF pages into smaller printable chunks",
description =
"This endpoint splits large or oddly-sized PDF pages into smaller chunks "
+ "suitable for printing on standard paper sizes (e.g., A4, Letter). "
+ "Divides each page into a grid of smaller pages using Apache PDFBox. "
+ "Input: PDF Output: ZIP-PDF Type: SISO")
"This endpoint splits large or oddly-sized PDF pages into smaller chunks"
+ " suitable for printing on standard paper sizes (e.g., A4, Letter). Divides each"
+ " page into a grid of smaller pages using Apache PDFBox.")
public Response posterPdf(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -35,6 +35,8 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.FormUtils;
@@ -60,12 +62,12 @@ public class RearrangePagesPDFController {
value = "/remove-pages",
resourceWeight = ResourceWeight.SMALL_WEIGHT)
@StandardPdfResponse
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Remove pages from a PDF file",
description =
"This endpoint removes specified pages from a given PDF file. Users can provide"
+ " a comma-separated list of page numbers or ranges to delete. Input:PDF"
+ " Output:PDF Type:SISO")
+ " a comma-separated list of page numbers or ranges to delete.")
public Response deletePages(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -254,13 +256,13 @@ public class RearrangePagesPDFController {
value = "/rearrange-pages",
resourceWeight = ResourceWeight.SMALL_WEIGHT)
@StandardPdfResponse
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Rearrange pages in a PDF file",
description =
"This endpoint rearranges pages in a given PDF file based on the specified page"
+ " order or custom mode. Users can provide a page order as a"
+ " comma-separated list of page numbers or page ranges, or a custom mode."
+ " Input:PDF Output:PDF")
+ " order or custom mode. Users can provide a page order as a comma-separated list"
+ " of page numbers or page ranges, or a custom mode.")
public Response rearrangePages(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -26,6 +26,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -49,11 +51,12 @@ public class RotationController {
value = "/rotate-pdf",
resourceWeight = ResourceWeight.SMALL_WEIGHT)
@StandardPdfResponse
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Rotate a PDF file",
description =
"This endpoint rotates a given PDF file by a specified angle. The angle must be"
+ " a multiple of 90. Input:PDF Output:PDF Type:SISO")
+ " a multiple of 90.")
public Response rotatePDF(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -31,6 +31,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -97,11 +99,12 @@ public class ScalePagesController {
value = "/scale-pages",
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.SMALL_WEIGHT)
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Change the size of a PDF page/document",
description =
"This operation takes an input PDF file and the size to scale the pages to in"
+ " the output PDF file. Input:PDF Output:PDF Type:SISO")
+ " the output PDF file.")
public Response scalePages(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -35,6 +35,9 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.FormUtils;
import stirling.software.common.util.GeneralUtils;
@@ -62,13 +65,13 @@ public class SplitPDFController {
value = "/split-pages",
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@MultiFileResponse
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
@Operation(
summary = "Split a PDF file into separate documents",
description =
"This endpoint splits a given PDF file into separate documents based on the"
+ " specified page numbers or ranges. Users can specify pages using"
+ " individual numbers, ranges, or 'all' for every page. Input:PDF"
+ " Output:PDF Type:SIMO")
+ " specified page numbers or ranges. Users can specify pages using individual"
+ " numbers, ranges, or 'all' for every page.")
public Response splitPdf(
@RestForm("fileInput") List<FileUpload> fileUpload,
@RestForm("fileId") String fileId,
@@ -39,6 +39,9 @@ import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.PdfMetadata;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
import stirling.software.common.util.ExceptionUtils;
@@ -103,11 +106,10 @@ public class SplitPdfByChaptersController {
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@MultiFileResponse
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
@Operation(
summary = "Split PDFs by Chapters",
description =
"Splits a PDF into chapters and returns a ZIP file. Input:PDF Output:ZIP-PDF"
+ " Type:SISO")
description = "Splits a PDF into chapters and returns a ZIP file.")
public Response splitPdf(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -38,6 +38,9 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@@ -63,13 +66,13 @@ public class SplitPdfBySectionsController {
value = "/split-pdf-by-sections",
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@MultiFileResponse
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
@Operation(
summary = "Split PDF pages into smaller sections",
description =
"Split each page of a PDF into smaller sections based on the user's choice"
+ " which page to split, and how to split"
+ " ( halves, thirds, quarters, etc.), both vertically and horizontally."
+ " Input:PDF Output:ZIP-PDF Type:SISO")
+ " which page to split, and how to split ( halves, thirds, quarters, etc.), both"
+ " vertically and horizontally.")
public Response splitPdf(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("fileId") String fileId,
@@ -34,6 +34,9 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolArity;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.FormUtils;
@@ -62,14 +65,14 @@ public class SplitPdfBySizeController {
consumes = MediaType.MULTIPART_FORM_DATA,
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@MultiFileResponse
@ToolIO(produces = ToolFormat.PDF, arity = ToolArity.SIMO)
@Operation(
summary = "Auto split PDF pages into separate documents based on size or count",
description =
"split PDF into multiple paged documents based on size/count, ie if 20 pages"
+ " and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB"
+ " (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF"
+ " Output:ZIP-PDF Type:SISO")
+ " and split into 5, it does 5 documents each 4 pages\r\n if 10MB and each page"
+ " is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts"
+ " 1.9MB but not 2.1MB)")
public Response autoSplitPdf(
@RestForm("fileInput") List<FileUpload> fileUpload,
@RestForm("fileId") String fileId,
@@ -28,6 +28,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
@@ -50,13 +52,13 @@ public class ToSinglePageController {
value = "/pdf-to-single-page",
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
@StandardPdfResponse
@ToolIO(produces = ToolFormat.PDF)
@Operation(
summary = "Convert a multi-page PDF into a single long page PDF",
description =
"This endpoint converts a multi-page PDF document into a single paged PDF"
+ " document. The width of the single page will be same as the input's"
+ " width, but the height will be the sum of all the pages' heights."
+ " Input:PDF Output:PDF Type:SISO")
+ " document. The width of the single page will be same as the input's width, but"
+ " the height will be the sum of all the pages' heights.")
public Response pdfToSinglePage(
@RestForm("fileInput") FileUpload fileUpload, @RestForm("fileId") String fileId)
throws IOException {
@@ -33,6 +33,8 @@ import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.enumeration.ResourceWeight;
import stirling.software.common.model.MultipartFile;
import stirling.software.common.model.multipart.FileUploadMultipartFile;
import stirling.software.common.model.tool.ToolFormat;
import stirling.software.common.model.tool.ToolIO;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
@@ -70,11 +72,12 @@ public class ConvertEbookToPDFController {
consumes = MediaType.MULTIPART_FORM_DATA,
value = "/ebook/pdf",
resourceWeight = ResourceWeight.LARGE_WEIGHT)
@ToolIO(accepts = ToolFormat.EBOOK, produces = ToolFormat.PDF)
@Operation(
summary = "Convert an eBook file to PDF",
description =
"This endpoint converts common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX)"
+ " to PDF using Calibre. Input:BOOK Output:PDF Type:SISO")
+ " to PDF using Calibre.")
public Response convertEbookToPdf(
@RestForm("fileInput") FileUpload fileUpload,
@RestForm("embedAllFonts") Boolean embedAllFonts,

Some files were not shown because too many files have changed in this diff Show More