mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
129
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4741e4f4a0 | ||
|
|
d959d49a1e | ||
|
|
2cf355c5cd | ||
|
|
c57a2a45de | ||
|
|
d30faf246b | ||
|
|
f6661a8f87 | ||
|
|
ceeec53df4 | ||
|
|
4ef2e3811c | ||
|
|
31d52d4c32 | ||
|
|
d55d8acbfa | ||
|
|
af97e1b27b | ||
|
|
01c908e95d | ||
|
|
0920ea9493 | ||
|
|
96207a7304 | ||
|
|
d93049db9f | ||
|
|
54e839ae65 | ||
|
|
3aca7a26f6 | ||
|
|
5fe7df3933 | ||
|
|
b1e857fd01 | ||
|
|
b92f88361e | ||
|
|
f6124223e4 | ||
|
|
3235645203 | ||
|
|
1f4cc2612d | ||
|
|
e539eb1ab1 | ||
|
|
1bb6961414 | ||
|
|
34694c6f5e | ||
|
|
0b7b4e02c2 | ||
|
|
74be5bf0ad | ||
|
|
8c00fffe18 | ||
|
|
c5da4177c4 | ||
|
|
ddc0ac0ced | ||
|
|
8bdd00b2fa | ||
|
|
3718af45ff | ||
|
|
41cbd97b48 | ||
|
|
993adaa3cd | ||
|
|
ead8a536d2 | ||
|
|
c22d9ecf58 | ||
|
|
d3708c1e63 | ||
|
|
1c055f3d18 | ||
|
|
4ab2505a6c | ||
|
|
658aa54c20 | ||
|
|
0a3f0c1814 | ||
|
|
f42c706c83 | ||
|
|
2c6d7545b2 | ||
|
|
3e008410e3 | ||
|
|
2a50f39175 | ||
|
|
e3ec92504d | ||
|
|
d7137fbd20 | ||
|
|
74bcd5570d | ||
|
|
12b55c2771 | ||
|
|
56a0084864 | ||
|
|
669d303e28 | ||
|
|
2f9c029f20 | ||
|
|
a05914ad55 | ||
|
|
2dcac5b340 | ||
|
|
6512498c70 | ||
|
|
8096d5ed77 | ||
|
|
ccf2b88094 | ||
|
|
a7373d0ff2 | ||
|
|
e55dad4851 | ||
|
|
e0b898c1a1 | ||
|
|
991fdacd52 | ||
|
|
5fa8d20612 | ||
|
|
1d2c5d2a44 | ||
|
|
88075a7f96 | ||
|
|
11809519a4 | ||
|
|
7b433dec22 | ||
|
|
a3870c4cf7 | ||
|
|
63c4d19965 | ||
|
|
a310ab8284 | ||
|
|
907a754d64 | ||
|
|
179bf8c3d6 | ||
|
|
c78c6523b6 | ||
|
|
36212b7ae3 | ||
|
|
8d363f838e | ||
|
|
4ada6e781c | ||
|
|
e410933f95 | ||
|
|
a77226f2a7 | ||
|
|
403bf550aa | ||
|
|
3543ac97c6 | ||
|
|
61feed02cb | ||
|
|
8cea88e963 | ||
|
|
d15dbcf519 | ||
|
|
e55c177fd1 | ||
|
|
e0b9ef2349 | ||
|
|
57063c51b5 | ||
|
|
33499d537e | ||
|
|
daf392521d | ||
|
|
4008161b93 | ||
|
|
5679967b1b | ||
|
|
05d3ba6f48 | ||
|
|
4c125a2edc | ||
|
|
e49d0268f2 | ||
|
|
c3a13f264f | ||
|
|
96ad92cec9 | ||
|
|
69b6a49e3a | ||
|
|
cc40a179a7 | ||
|
|
403627101f | ||
|
|
33dad0f81a | ||
|
|
1bc548d6c0 | ||
|
|
b9069a1889 | ||
|
|
a30d524ec2 | ||
|
|
ee26b35b33 | ||
|
|
860bd6e63d | ||
|
|
4b572852c9 | ||
|
|
4665cceeb3 | ||
|
|
90e8e34199 | ||
|
|
fe6c6c0b49 | ||
|
|
e07eefc013 | ||
|
|
10f4ecd09d | ||
|
|
20b25ad760 | ||
|
|
185ac88b30 | ||
|
|
d51228af63 | ||
|
|
06da9597af | ||
|
|
611574fb54 | ||
|
|
2591faa0ba | ||
|
|
cec00ab78a | ||
|
|
7e068622c9 | ||
|
|
4d69bcea32 | ||
|
|
9fe815bbfd | ||
|
|
638f7e8c6c | ||
|
|
f0c7fbdac9 | ||
|
|
f24c9e0501 | ||
|
|
5eec2b4446 | ||
|
|
0aad71b127 | ||
|
|
5157b6ef1c | ||
|
|
5dd6d7be69 | ||
|
|
74474fa967 | ||
|
|
266aed750d |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"${CLAUDE_PROJECT_DIR}/scripts/lint/comment-lint-hook.mjs"
|
||||
],
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
build/
|
||||
*/build/
|
||||
**/build/
|
||||
# ...but re-include the Quarkus runner-jar so docker/quarkus/Dockerfile can layer it on the base image
|
||||
!app/core/build/*-runner.jar
|
||||
out/
|
||||
target/
|
||||
**/target/
|
||||
|
||||
@@ -20,6 +20,7 @@ Closes #(issue_number)
|
||||
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable)
|
||||
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable)
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] Every comment I added says something the code does not ([guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/CODE_COMMENTS.md))
|
||||
- [ ] My changes generate no new warnings
|
||||
|
||||
### Documentation
|
||||
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -220,6 +220,42 @@ jobs:
|
||||
echo "app_short=${APP_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# The Stirling account previews connect to. Derived from the ref rather than stored as a URL
|
||||
# so it cannot drift from the key: a mismatched pair is accepted by the browser and rejected
|
||||
# by Supabase, surfacing much later as "session expired" on Usage rather than at sign-in.
|
||||
# Secret only to match Saas-Dev-Deploy.yml, which owns the same value; a project ref is not
|
||||
# itself sensitive, which is why SAAS_API_BASE_URL next to it is a plain variable.
|
||||
- name: Resolve Stirling account config
|
||||
id: saas
|
||||
env:
|
||||
PROJECT_REF: ${{ secrets.SAAS_DB_PROJECT_REF }}
|
||||
API_BASE_OVERRIDE: ${{ vars.SAAS_API_BASE_URL }}
|
||||
run: |
|
||||
# Set, this is the one value both halves use: the browser's portal reads and the backend's
|
||||
# register/entitlement calls have to land on the same SaaS, and nothing checks that they
|
||||
# do. Unset, only the backend gets a base, from its own compiled-in default.
|
||||
API_BASE="${API_BASE_OVERRIDE:-https://stirling.com/app}"
|
||||
echo "backend_base=${API_BASE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ -z "${PROJECT_REF}" ]; then
|
||||
echo "Not configured for this environment: the preview will build without a Stirling"
|
||||
echo "account, and the connect dialog will say so. To wire one up, set on the"
|
||||
echo "pr-preview environment the secrets SAAS_DB_PROJECT_REF and"
|
||||
echo "SAAS_SUPABASE_PUBLISHABLE_KEY, both from the same Supabase project."
|
||||
echo "supabase_url=" >> "$GITHUB_OUTPUT"
|
||||
echo "frontend_base=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Only whether, not which: the ref is a secret here, so Actions masks it out of any
|
||||
# line it appears in, derived URL included.
|
||||
echo "Stirling account configured, at ${API_BASE}."
|
||||
echo "supabase_url=https://${PROJECT_REF}.supabase.co" >> "$GITHUB_OUTPUT"
|
||||
# Deliberately the override and not API_BASE: the backend's default is a subpath URL
|
||||
# nobody has confirmed answers /api/v1, and prod CORS does not list preview hostnames,
|
||||
# so portal reads stay off until someone sets a base they have checked. Empty leaves the
|
||||
# committed .env default alone, which is the clean "not configured" state.
|
||||
echo "frontend_base=${API_BASE_OVERRIDE}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if image exists
|
||||
id: check-image
|
||||
run: |
|
||||
@@ -246,6 +282,9 @@ jobs:
|
||||
build-args: |
|
||||
VERSION_TAG=v2-alpha
|
||||
BUILD_PORTAL=${{ env.BUILD_PORTAL }}
|
||||
VITE_SUPABASE_URL=${{ steps.saas.outputs.supabase_url }}
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${{ secrets.SAAS_SUPABASE_PUBLISHABLE_KEY }}
|
||||
VITE_SAAS_API_URL=${{ steps.saas.outputs.frontend_base }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
@@ -279,6 +318,13 @@ jobs:
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_ENABLED: "true"
|
||||
STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL: "${{ steps.saas.outputs.backend_base }}"
|
||||
# Off so preview traffic never accrues against a real wallet or trips its cap. The
|
||||
# 402 gate is separate and stays on, so gating is still testable here.
|
||||
STIRLING_BILLING_ACCOUNT_LINK_METERING_ENABLED: "false"
|
||||
# Stated rather than inferred from the request: the callback has to come back to the
|
||||
# preview hostname, not to the container's own :8080 behind this proxy.
|
||||
SYSTEM_FRONTENDURL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SECURITY_INITIALLOGIN_USERNAME: "${TEST_LOGIN_USERNAME}"
|
||||
SECURITY_INITIALLOGIN_PASSWORD: "${TEST_LOGIN_PASSWORD}"
|
||||
@@ -353,7 +399,7 @@ jobs:
|
||||
|
||||
- name: Install Task for Storybook
|
||||
if: steps.sb-changes.outputs.storybook == 'true'
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Build and deploy Storybook
|
||||
id: storybook
|
||||
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Run Gradle Command
|
||||
run: |
|
||||
if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
|
||||
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
|
||||
@@ -34,10 +34,9 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: ai-engine
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Quality-check engine
|
||||
id: engine-check
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Check Java formatting (Spotless)
|
||||
# Runs once per matrix combination - pick the cheapest leg
|
||||
# (core - no proprietary, no saas) so we don't wait for the
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (needed for playwright's vite preview webServer)
|
||||
|
||||
@@ -42,7 +42,6 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: generated-models
|
||||
|
||||
- name: Restore cache Gradle User Home
|
||||
if: inputs.use_shared_cache
|
||||
@@ -76,7 +75,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Verify generated models are up to date
|
||||
id: models-check
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Check licenses for compatibility
|
||||
run: task backend:licenses:check
|
||||
env:
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Generate OpenAPI documentation
|
||||
run: task backend:swagger
|
||||
env:
|
||||
|
||||
@@ -48,16 +48,18 @@ jobs:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
run: ./gradlew :stirling-pdf:bootJar -PnoSpotless --no-daemon
|
||||
run: ./gradlew :stirling-pdf:quarkusBuild -PnoSpotless --no-daemon
|
||||
|
||||
- name: Locate built JAR
|
||||
id: jar
|
||||
run: |
|
||||
jar=$(find app/core/build/libs -maxdepth 1 -name 'Stirling-PDF*.jar' -o -name 'stirling-pdf*.jar' 2>/dev/null \
|
||||
| grep -vE '(-plain|-sources)\.jar$' | head -n 1)
|
||||
# Quarkus (quarkus.package.jar.type=uber-jar) emits a standalone runnable
|
||||
# jar at app/core/build/<name>-runner.jar, replacing the Spring Boot bootJar
|
||||
# that used to land in app/core/build/libs.
|
||||
jar=$(find app/core/build -maxdepth 1 -name '*-runner.jar' 2>/dev/null | head -n 1)
|
||||
if [[ -z "$jar" ]]; then
|
||||
echo "::error::No JAR under app/core/build/libs"
|
||||
ls -lah app/core/build/libs || true
|
||||
echo "::error::No *-runner.jar under app/core/build"
|
||||
ls -lah app/core/build || true
|
||||
exit 1
|
||||
fi
|
||||
# Absolute path - the migration script pushd's into a temp workdir
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
# runtime token isn't exposed) since the docker driver can't use it.
|
||||
- name: Set up Docker Buildx
|
||||
if: inputs.docker-base-changed != 'true'
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
# Expose ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL for docker buildx type=gha cache backend.
|
||||
- name: Expose GitHub runtime for Buildx cache
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Install Playwright (chromium only)
|
||||
run: task e2e:install -- chromium
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Build frontend (production bundle for vite preview)
|
||||
env:
|
||||
VITE_BUILD_FOR_PREVIEW: "1"
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: a11y gate (changed stories)
|
||||
run: task frontend:storybook:a11y:changed -- origin/${{ github.base_ref || 'main' }}
|
||||
- name: Upload scan reports
|
||||
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
run: npm ci --ignore-scripts --audit=false --fund=false
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Generate frontend license report (Push only)
|
||||
if: github.event_name == 'push'
|
||||
@@ -367,7 +367,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Quality-check frontend
|
||||
id: frontend-check
|
||||
run: task frontend:check:all
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Build JAR
|
||||
run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
# Build the universal JRE before desktop:prepare so the jlink:runtime
|
||||
# task short-circuits on its `test -d runtime/jre` status check.
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Install all Playwright browsers
|
||||
run: task e2e:install
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: a11y gate (every story, ${{ matrix.theme }})
|
||||
run: task frontend:storybook:a11y:${{ matrix.theme }}
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
engine/uv.lock
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Start the fat image with login and storage enabled
|
||||
run: docker compose -f docker/embedded/compose/test_cicd.yml up -d --build
|
||||
|
||||
@@ -31,10 +31,14 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: pre-commit
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Run pre-commit checks
|
||||
run: task pre-commit
|
||||
|
||||
# The fixture corpus checks the comment rules themselves, so it runs here
|
||||
# rather than on every local commit.
|
||||
- name: Check the comment-lint fixture corpus
|
||||
run: task pre-commit:comment-lint:selftest
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
|
||||
@@ -85,10 +85,10 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -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@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
SWAGGERHUB_USER: "Frooodle"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -59,14 +59,13 @@ jobs:
|
||||
cache-dependency-glob: |
|
||||
engine/pyproject.toml
|
||||
engine/uv.lock
|
||||
cache-suffix: sync-files
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
uv sync --project engine --locked --group tools
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Sync translation TOML files
|
||||
run: |
|
||||
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
distribution: ${{ matrix.platform == 'windows-11-arm' && 'microsoft' || 'temurin' }}
|
||||
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
|
||||
- name: Build universal macOS JRE
|
||||
if: matrix.platform == 'macos-15'
|
||||
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
|
||||
uses: go-task/setup-task@a00fbb05ce67b35648be3c78cbc9fd85354c757e # v2.2.0
|
||||
- name: Build application
|
||||
run: task backend:build
|
||||
env:
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Set base image and platform for this build
|
||||
id: build-params
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Build docker/unoserver/Dockerfile
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
|
||||
+30
-6
@@ -47,10 +47,15 @@ SwaggerDoc.json
|
||||
# Runtime storage for uploaded files and user data (not Java source code)
|
||||
app/core/storage/
|
||||
|
||||
# Frontend build artifacts copied to backend static resources
|
||||
# These are generated by npm build and should not be committed
|
||||
app/core/src/main/resources/static/assets/
|
||||
# Frontend build artifacts copied to Quarkus static resources
|
||||
# Generated by `npm build` + the copyFrontendAssets/copyFrontendIndexHtml tasks; never committed.
|
||||
# The React bundle goes to META-INF/resources/ (Quarkus serves these over HTTP); index.html is the
|
||||
# only generated file in static/ (ReactRoutingController serves it). See app/core/build.gradle.
|
||||
app/core/src/main/resources/META-INF/resources/
|
||||
app/core/src/main/resources/static/index.html
|
||||
# Migration cleanup: earlier builds emitted the whole bundle into static/. Keep these ignored so
|
||||
# any stale generated assets left in static/ are not accidentally committed.
|
||||
app/core/src/main/resources/static/assets/
|
||||
# Prerendered per-route SPA pages (OG/social-preview), e.g. compress.html. api-landing.html is source.
|
||||
app/core/src/main/resources/static/*.html
|
||||
!app/core/src/main/resources/static/api-landing.html
|
||||
@@ -82,7 +87,7 @@ app/core/src/main/resources/static/css/cookieconsentCustomisation.css
|
||||
app/core/src/main/resources/static/mockServiceWorker.js
|
||||
app/core/src/main/resources/static/js/thirdParty/cookieconsent.umd.js
|
||||
app/core/src/main/resources/static/images/google-drive.svg
|
||||
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
|
||||
# Note: Keep backend-managed files like fonts/, css/, js/, etc.
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
@@ -298,8 +303,27 @@ docs/type3/signatures/
|
||||
|
||||
**/application-dev-local.properties
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
# AI agent session/local files - may contain tokens and secrets. .claude keeps
|
||||
# its two shared pieces: settings.json (the comment-lint hook) and skills/.
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
!.claude/skills/
|
||||
.claude/settings.local.json
|
||||
.agents/
|
||||
.cursor/
|
||||
.codex/
|
||||
.opencode/
|
||||
.copilot/
|
||||
.cline/
|
||||
.continue/
|
||||
.windsurf/
|
||||
.junie/
|
||||
.pi/
|
||||
.roo/
|
||||
.augment/
|
||||
.aider*
|
||||
CLAUDE.local.md
|
||||
skills-lock.json
|
||||
|
||||
# Playwright MCP screenshots / traces
|
||||
.playwright-mcp/
|
||||
|
||||
@@ -11,6 +11,7 @@ vars:
|
||||
'.github/scripts/*.py'
|
||||
'app/core/src/main/resources/static/python/*.py'
|
||||
':(exclude)*split_photos.py'
|
||||
':(exclude)scripts/lint/fixtures/*'
|
||||
SPELL_FILES: >-
|
||||
'*.html'
|
||||
'*.css'
|
||||
@@ -59,6 +60,7 @@ tasks:
|
||||
- task: gitleaks
|
||||
- task: whitespace
|
||||
- task: toml-sort
|
||||
- task: comment-lint
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix formatting, spelling, and secrets issues across the repo"
|
||||
@@ -75,6 +77,7 @@ tasks:
|
||||
vars: { FIX: '1' }
|
||||
- task: codespell
|
||||
- task: gitleaks
|
||||
- task: comment-lint
|
||||
|
||||
install:
|
||||
desc: "Install the pinned pre-commit Python tools"
|
||||
@@ -111,7 +114,7 @@ tasks:
|
||||
codespell:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- uv run --project engine --locked --group pre-commit codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment --quiet-level=2 $(git ls-files {{.SPELL_FILES}})
|
||||
- uv run --project engine --locked --group pre-commit codespell --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment,vertx --quiet-level=2 $(git ls-files {{.SPELL_FILES}})
|
||||
|
||||
toml-sort:
|
||||
deps: [install]
|
||||
@@ -130,6 +133,85 @@ tasks:
|
||||
cmds:
|
||||
- "{{.GITLEAKS_BIN}} git --pre-commit --redact --staged --verbose"
|
||||
|
||||
comment-lint:
|
||||
desc: "Check comment quality on the lines this branch adds"
|
||||
summary: |
|
||||
Blocks a comment that restates the code below it, a section banner, or a
|
||||
block of commented-out code. Everything else it reports is advisory.
|
||||
|
||||
Scoped to added lines, so touching an old file never surfaces the standing
|
||||
backlog. The standard is devGuide/CODE_COMMENTS.md.
|
||||
|
||||
With no arguments it diffs the working tree against HEAD, which is what a
|
||||
pre-commit run wants: the lines you are about to commit. On a CI pull request
|
||||
it diffs against the target branch instead, via GITHUB_BASE_REF.
|
||||
|
||||
To ask what a whole branch adds instead, use the branch variant, which
|
||||
needs no argument passing:
|
||||
task comment-lint:branch
|
||||
|
||||
Full tree (report only): task pre-commit:comment-lint:all
|
||||
Fixture corpus: task pre-commit:comment-lint:selftest
|
||||
# Depends on the frontend install because the .ts/.tsx half of the rule set
|
||||
# runs as an oxlint plugin. Without it the TS engine warns and skips, which
|
||||
# would leave the frontend silently unchecked on CI.
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs {{.CLI_ARGS}}
|
||||
|
||||
comment-lint:branch:
|
||||
desc: "Check comment quality on everything this branch adds over its base"
|
||||
summary: |
|
||||
Like `task comment-lint`, but scoped to the whole branch rather than to
|
||||
uncommitted work, so it still reports after you commit.
|
||||
|
||||
Exists as its own task because passing `-- --since origin/main` through Task
|
||||
is not portable: with the npm build of Task the launcher is a PowerShell
|
||||
script, and PowerShell strips the `--` before Task sees it, leaving Task to
|
||||
print its own usage.
|
||||
|
||||
Override the base with BASE=<ref>.
|
||||
vars:
|
||||
BASE: '{{.BASE | default "origin/main"}}'
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --since {{.BASE}}
|
||||
|
||||
comment-lint:ci:
|
||||
desc: "Comment gate as CI runs it: fixture corpus, then the diff"
|
||||
summary: |
|
||||
The corpus checks the rules themselves rather than the code under review, so
|
||||
it belongs on CI and not on every local commit. Run this before changing a
|
||||
rule, and let CI run it on every pull request.
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --selftest
|
||||
- node scripts/lint/comment-lint.mjs {{.CLI_ARGS}}
|
||||
|
||||
comment-lint:hook:
|
||||
desc: "Comment gate for the editor hook: everything this turn changed"
|
||||
summary: |
|
||||
Same scope as `task comment-lint`, kept as its own name so the hook has a
|
||||
stable entry point and the taskfile shows every way the linter is invoked.
|
||||
|
||||
Not in the frontend-install dependency chain on purpose: this runs at the end
|
||||
of every turn, so it stays as short as it can be. If oxlint is missing the TS
|
||||
half warns and skips.
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs
|
||||
|
||||
comment-lint:all:
|
||||
desc: "Report every comment finding in the tree (never fails)"
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --all
|
||||
|
||||
comment-lint:selftest:
|
||||
desc: "Check both comment-lint engines against the fixture corpus"
|
||||
deps: [":frontend:install"]
|
||||
cmds:
|
||||
- node scripts/lint/comment-lint.mjs --selftest
|
||||
|
||||
gitleaks-bin:
|
||||
internal: true
|
||||
desc: "Ensure the pinned, checksum-verified gitleaks binary is cached in .task/bin"
|
||||
|
||||
@@ -21,6 +21,43 @@ Task `desc:` fields should describe **what** the task does, not **how** it does
|
||||
- `task docker:build` — build standard Docker image
|
||||
- `task docker:up` — start Docker compose stack
|
||||
|
||||
## Comments
|
||||
|
||||
A comment must carry information the code cannot. If a reader could derive it from the code in front of them, delete it.
|
||||
|
||||
Comment the current state. Not what the code used to do, not what changed, not why it changed: git holds that. Where history explains the shape, state the reason instead, so "this used to reimplement the modal internals" becomes "thin wrapper over the shared Modal: duplicating its portal and focus trap is how dialogs drift apart". Future state goes in a TODO with an issue.
|
||||
|
||||
Write a comment when it does one of these four jobs:
|
||||
|
||||
- **Contract.** What a caller must know that the signature cannot say: preconditions, invariants, units, ownership and lifetime, thread-safety, error semantics, side effects. Document the contract of everything a caller outside the file can reach, and nothing else. Goes on the type/method/module as Javadoc, JSDoc, or a docstring.
|
||||
- **Why.** The constraint the code satisfies, the bug it avoids, the alternative rejected and the reason.
|
||||
- **Hazard.** "Must stay in sync with X", "order matters because Y", "do not remove, it prevents Z".
|
||||
- **Map.** A short orientation at the top of a genuinely complex file: what it owns, and what it deliberately does not.
|
||||
|
||||
Never write:
|
||||
|
||||
- A comment that restates the next line. `// Handle drag start` above `handleDragStart` is noise.
|
||||
- Section banners or position markers: `// --- Types ---`, `// Helpers`, `// =====`.
|
||||
- Step narration in a function body (`// Step 1:`, `// Then we`). If the steps need labels they need names: extract functions. Numbering a genuinely numbered thing, like a wizard step, is fine.
|
||||
- Commented-out code. Delete it.
|
||||
- Doc tags that restate the signature. `@param blob - The blob` says nothing; omit the tag rather than pad it.
|
||||
- Docs on self-explanatory members with no constraint to state.
|
||||
|
||||
Two tests before keeping a comment:
|
||||
|
||||
- **Delete it.** Is any information lost? If not, it stays deleted.
|
||||
- **Could a name carry it instead?** A better identifier, an extracted function, or a named constant beats a comment. Prefer the code change.
|
||||
|
||||
A comment at the end of a line usually decodes that line, and that is worth keeping: `{0x25, 0x50} // "%PDF"`, `50L * 1024 * 1024 // 50 MB`. The rules that compare a comment against the code below it do not apply there, but a trailing TODO or a trailing bit of history is judged like any other.
|
||||
|
||||
A reference is supplementary, never load-bearing: the comment must survive deleting it. `// See #1234` is a dead end; `// saving first loses every annotation (#6865)` is not. Prefer a spec (`RFC 3161`) or CVE where one applies.
|
||||
|
||||
A TODO needs an issue, not an owner: `// TODO(#1234): re-enable the gate once account syncing lands`. If it is not worth an issue, it is not worth a TODO. A question is not a TODO.
|
||||
|
||||
A comment block over ~12 lines outside a file or type header usually means the code needs restructuring, or that the prose is product documentation and belongs in the docs repo.
|
||||
|
||||
`task comment-lint` checks the mechanical part of this on the lines you add, and runs inside `task pre-commit`. Reasoning, worked examples and the linter's own rules: @devGuide/CODE_COMMENTS.md
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Build and Test
|
||||
@@ -70,7 +107,7 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
- Avoid nested functions and nested classes unless the language construct requires them.
|
||||
- Prefer composition to inheritance when combining concepts.
|
||||
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
|
||||
- Add comments sparingly and only when they explain non-obvious intent.
|
||||
- Comments follow the repo-wide rules in the "Comments" section above.
|
||||
|
||||
#### Python Typing and Models
|
||||
- Deserialize into Pydantic models as early as possible.
|
||||
|
||||
@@ -42,6 +42,7 @@ Please make sure your Pull Request adheres to the following guidelines:
|
||||
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
|
||||
- Commits should be clear, concise, and easy to understand.
|
||||
- References to the Issue number in the Pull Request and/or Commit message.
|
||||
- Every comment in the diff should say something the code does not. See [Code comments](devGuide/CODE_COMMENTS.md); `task comment-lint` checks the mechanical part.
|
||||
|
||||
## Translations
|
||||
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
# Stirling-PDF: Spring Boot → Quarkus Migration — Continuation Handoff
|
||||
|
||||
> **Purpose:** everything needed to resume this migration in a fresh session. Read this top-to-bottom
|
||||
> before touching anything. Companion doc `migration-report.md` has the higher-level summary; this
|
||||
> file is the working/continuation guide with the concrete state, commands, fixed bugs, remaining
|
||||
> bugs, and the recurring patterns you need to apply.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR status
|
||||
|
||||
- **2026-06-19 — merged `main` (136 commits) into `migration/run-02`.** Resolved 69 conflicts and
|
||||
did **full Spring removal + Quarkus migration of every newly pulled-in file** (36 Spring-bearing
|
||||
files: 21 proprietary + 15 saas). Net effect on main: legacy credits engine deleted (#6687,
|
||||
replaced by PAYG #6589); the proprietary **policy** subsystem and the new **PAYG** subsystem
|
||||
migrated to CDI/JAX-RS/Panache. Verified: 0 conflict markers, 0 `org.springframework` imports in
|
||||
any main source. **`core` + `proprietary` compile; `proprietary` Quarkus-augments + boots + serves
|
||||
real traffic; `saas` now compiles AND augments too** (previously ~28 CDI issues). Cross-file fix:
|
||||
`PolicyExecutor`/`DownstreamEntitlementError` now carry HTTP status+body via
|
||||
`jakarta.ws.rs.WebApplicationException` (was Spring `RestClientResponseException`);
|
||||
`AiWorkflowService.paygLimitResponseOrNull` rewired to it. Repo `save()` shim added to the Panache
|
||||
repos whose callers/tests expect Spring-Data `save()`.
|
||||
- **Branch:** `migration/run-01` (all work committed locally, **nothing pushed** — `origin` is the
|
||||
public `Stirling-Tools/Stirling-PDF` repo; do not push without the owner's say-so).
|
||||
- **Default flavor (`proprietary`):** compiles, Quarkus-augments, boots, and serves real traffic in
|
||||
Docker. ✅
|
||||
- **Cucumber API e2e (full-tool Docker image):** baselines, newest first:
|
||||
- **Run 2 (login off, this session's fixes, no JWT mechanism): 223 / 258 pass**, 35 failed, 80
|
||||
skipped. Up from the prior **183 / 258** baseline (+40). Eliminated buckets: split
|
||||
`PDF corrupted` 8→0, `FileAlreadyExists` 8→0, `Admin login failed (500)` 17→0.
|
||||
- **Run 3 (login off + `V2=true` + the new JWT Bearer mechanism): the 80 JWT/admin scenarios now
|
||||
RUN (0 skipped)** because the `login → /me` probe passes. See §6.E / "Session 2". Final tally
|
||||
recorded in §9.
|
||||
- **Stack:** Quarkus 3.33.2 LTS, **Java 25** (mandatory — see §2), Hibernate ORM Panache,
|
||||
quarkus-rest (RESTEasy Reactive), quarkus-oidc, quarkus-undertow (servlet, for filters), OpenSAML 5.
|
||||
- **`saas` flavor:** compiles but full augmentation has ~28 CDI issues (design-level follow-up).
|
||||
- **JWT Bearer login:** ✅ works end-to-end (token issue + validate → `SecurityIdentity`, role
|
||||
mapping, `@RolesAllowed`). **OAuth2/OIDC + SAML2 SSO:** ✅ both work end-to-end against the
|
||||
`testing/compose` Keycloak stacks; `validate-oauth-test.sh` and `validate-saml-test.sh` both pass
|
||||
(see §6.F). The default e2e Docker image + build helper are committed at `docker/quarkus/` (§3.3).
|
||||
|
||||
---
|
||||
|
||||
## 1. Repo / flavor layout
|
||||
|
||||
Multi-module Gradle build, three selectable flavors via `STIRLING_FLAVOR` (or `ENABLE_SAAS` /
|
||||
`DISABLE_ADDITIONAL_FEATURES`):
|
||||
|
||||
| Flavor | Modules included | Notes |
|
||||
|--------|------------------|-------|
|
||||
| `core` | `:common`, `:stirling-pdf` (core) | OSS only |
|
||||
| `proprietary` (**default**) | + `:proprietary` | what all the e2e work targets |
|
||||
| `saas` | + `:saas` | opt-in: `STIRLING_FLAVOR=saas`; not yet augmentable |
|
||||
|
||||
Module → directory:
|
||||
- `:stirling-pdf` → `app/core` (the runnable Quarkus app; applies the `io.quarkus` gradle plugin)
|
||||
- `:common` → `app/common` (library; CDI beans / JAX-RS / entities)
|
||||
- `:proprietary` → `app/proprietary` (library)
|
||||
- `:saas` → `app/saas` (library, only on saas flavor)
|
||||
|
||||
Quarkus only discovers beans/entities in dependency jars that carry a **Jandex index**; the library
|
||||
modules are indexed via `quarkus.index-dependency.*` in
|
||||
`app/core/src/main/resources/application.properties`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Java 25 is mandatory (don't regress this)
|
||||
|
||||
- The build uses a **JDK 25 toolchain** (`build.gradle` `subprojects { java { toolchain = 25 } }`).
|
||||
- The app is compiled to **class-file version 69 (Java 25)** — it will NOT run on JDK 21.
|
||||
- **The host's default `java` on the PATH is JDK 21.** Use the toolchain JDK 25 explicitly:
|
||||
- `JAVA_HOME` points to a Temurin 25 JDK (`C:\Users\systo\scoop\apps\temurin25-jdk\current`).
|
||||
- In Git Bash run the jar with `"$JAVA_HOME/bin/java" -jar ...` (host `java` = 21 → `UnsupportedClassVersionError`).
|
||||
- The Docker base image `stirlingtools/stirling-pdf-base:1.0.2` ships **Temurin 25.0.2** — so the
|
||||
container runtime is JDK 25 already. Keep it that way; do not switch the base image to a JRE < 25.
|
||||
- Gradle build images / CI also pin `gradle:9.3.1-jdk25` and `eclipse-temurin:25-jre-noble`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Build → package → run → test (the exact loop)
|
||||
|
||||
### 3.1 Build the runnable jar
|
||||
```bash
|
||||
./gradlew :stirling-pdf:quarkusBuild -x test --console=plain
|
||||
```
|
||||
- Produces the **runnable uber-jar** at: `app/core/build/stirling-pdf-2.12.0-runner.jar`
|
||||
- `Main-Class: stirling.software.SPDF.SPDFApplication`.
|
||||
- ⚠️ **GOTCHA:** `app/core/build/libs/stirling-pdf-2.12.0.jar` is the *plain* (non-runnable) jar with
|
||||
an empty manifest. The upstream `docker/embedded/Dockerfile` copies `libs/*.jar` — that's now the
|
||||
WRONG jar. Always use the `-runner.jar`. (`quarkus.package.jar.type=uber-jar` is set in
|
||||
application.properties.)
|
||||
- ⚠️ If the build fails with `Unable to delete .../-runner.jar`, a previous `java -jar` is still
|
||||
holding it. Kill it: PowerShell `Get-CimInstance Win32_Process -Filter "Name='java.exe'" | ?{ $_.CommandLine -like '*stirling-pdf-2.12.0-runner*' } | %{ Stop-Process -Id $_.ProcessId -Force }`.
|
||||
|
||||
### 3.2 Run standalone for a quick boot check (host JDK 25, fastest)
|
||||
```bash
|
||||
SECURITY_ENABLELOGIN=false QUARKUS_HTTP_PORT=8095 \
|
||||
QUARKUS_DATASOURCE_JDBC_URL="jdbc:h2:mem:t;DB_CLOSE_DELAY=-1;MODE=PostgreSQL" \
|
||||
nohup "$JAVA_HOME/bin/java" -jar app/core/build/stirling-pdf-2.12.0-runner.jar > /tmp/boot.log 2>&1 &
|
||||
# success line in log: "Stirling-PDF running on port: 8095" (this app does NOT print Quarkus' "Listening on")
|
||||
```
|
||||
Health: `curl localhost:8095/api/v1/info/status` → `{"version":"2.12.0","status":"UP"}`.
|
||||
|
||||
### 3.3 The "normal" Docker image (full tools) — what the cucumber e2e uses
|
||||
The upstream `docker/embedded/Dockerfile` is **Spring-Boot-specific** (uses
|
||||
`java -Djarmode=tools -jar app.jar extract --layers` + `spring-boot-loader` layers) and does NOT
|
||||
work with the Quarkus jar. For e2e I built an ad-hoc image layering the runner-jar on the prebuilt
|
||||
**base image** (which already has Java 25 + LibreOffice + Tesseract + qpdf + Ghostscript + Calibre +
|
||||
Python). **This Dockerfile lives in a temp dir and needs to be committed into the repo** (see §6 TODO).
|
||||
|
||||
Build context (currently ephemeral at the bash path `/tmp/sp-full` =
|
||||
`C:\Users\systo\AppData\Local\Temp\sp-full`): `app.jar` (the runner jar), `fonts/*.ttf`, and this
|
||||
Dockerfile:
|
||||
```dockerfile
|
||||
FROM stirlingtools/stirling-pdf-base:1.0.2 # Java 25 + all tools
|
||||
WORKDIR /app
|
||||
COPY --chown=1000:1000 app.jar /app/app.jar
|
||||
COPY fonts/*.ttf /usr/share/fonts/truetype/
|
||||
RUN fc-cache -f \
|
||||
&& mkdir -p /storage \
|
||||
&& chown stirlingpdfuser:stirlingpdfgroup /storage /app \
|
||||
&& ln -sf /configs /app/configs && ln -sf /logs /app/logs \
|
||||
&& ln -sf /customFiles /app/customFiles && ln -sf /pipeline /app/pipeline \
|
||||
&& ln -sf /storage /app/storage \
|
||||
&& chown -h stirlingpdfuser:stirlingpdfgroup /app/configs /app/logs /app/customFiles /app/pipeline /app/storage
|
||||
ENV HOME=/home/stirlingpdfuser STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf TEMP=/tmp/stirling-pdf TMP=/tmp/stirling-pdf \
|
||||
SAL_TMP=/tmp/stirling-pdf/libre DBUS_SESSION_BUS_ADDRESS=/dev/null \
|
||||
JAVA_OPTS="-XX:+UseG1GC -Djava.awt.headless=true" \
|
||||
QUARKUS_HTTP_HOST=0.0.0.0 QUARKUS_HTTP_PORT=8080
|
||||
EXPOSE 8080/tcp
|
||||
STOPSIGNAL SIGTERM
|
||||
USER stirlingpdfuser
|
||||
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
|
||||
```
|
||||
Stage + build + run:
|
||||
```bash
|
||||
# stage (bash /tmp resolves to %LOCALAPPDATA%\Temp)
|
||||
mkdir -p /tmp/sp-full/fonts
|
||||
cp app/core/build/stirling-pdf-2.12.0-runner.jar /tmp/sp-full/app.jar
|
||||
cp app/core/src/main/resources/static/fonts/*.ttf /tmp/sp-full/fonts/
|
||||
# (write the Dockerfile above to C:\Users\systo\AppData\Local\Temp\sp-full\Dockerfile)
|
||||
cd /tmp/sp-full && docker build -t stirling-pdf-quarkus:full .
|
||||
docker rm -f sp-e2e
|
||||
docker run -d --name sp-e2e -p 8080:8080 \
|
||||
-e SECURITY_ENABLELOGIN=false -e METRICS_ENABLED=true \
|
||||
-e SYSTEM_DEFAULTLOCALE=en-US -e SYSTEM_MAXFILESIZE=100 \
|
||||
stirling-pdf-quarkus:full
|
||||
# wait for: curl localhost:8080/api/v1/info/status == 200
|
||||
```
|
||||
Base image was pulled with `docker pull stirlingtools/stirling-pdf-base:1.0.2`.
|
||||
|
||||
### 3.4 Run the cucumber (behave) suite
|
||||
- Tests live in `testing/cucumber/` — Python **behave** (BDD), pure HTTP via `requests` (no browser).
|
||||
- **Target URL is hardcoded `http://localhost:8080`** in `features/steps/step_definitions.py`
|
||||
(lines ~584/592/601) and `features/environment.py`. Easiest is to run the app on 8080.
|
||||
- `behave.ini` excludes `features/(enterprise|payg)` and tag `~@manual` by default.
|
||||
- `environment.py` probes `/api/v1/auth/login` (admin/stirling) at startup; if JWT/login is not
|
||||
functional (login disabled / V2) it **skips** all `@jwt @login @me @refresh @token @mfa @apikey
|
||||
@admin_settings @audit @signature @team @user_mgmt` scenarios → ~80 skips. That's expected.
|
||||
|
||||
Install deps + run:
|
||||
```bash
|
||||
cd testing/cucumber
|
||||
pip install -r requirements.txt # behave, requests, pypdf, reportlab, psycopg, pillow, ...
|
||||
TEST_CONTAINER_NAME=sp-e2e TEST_REPORT_DIR=/tmp python -m behave --no-capture --format progress2
|
||||
# single feature: python -m behave features/general.feature
|
||||
# one scenario: python -m behave features/general.feature:22 --format plain
|
||||
```
|
||||
The official CI driver is `testing/test.sh` (builds images via `docker/embedded/Dockerfile.*` and
|
||||
runs behave) — it will need the Dockerfile fixes from §6 before it works on Quarkus.
|
||||
|
||||
---
|
||||
|
||||
## 4. Bugs FIXED this session (with the *why*, so you can spot siblings)
|
||||
|
||||
### Session 2 (branch `claude/happy-chaplygin-906fe7`, fast-forwarded from `migration/run-01`)
|
||||
|
||||
Newest first. These took the login-off suite **183 → 223** and then wired JWT so the **80 skipped
|
||||
JWT/admin scenarios run** (run 3, §9):
|
||||
|
||||
1. **JWT Bearer → `SecurityIdentity` was never populated** → every user-scoped endpoint that reads
|
||||
`SecurityIdentity.getPrincipal()` (folders, files, `/me`, user/team settings…) failed, and the
|
||||
`environment.py` probe (`login → /me`) failed so ~80 scenarios auto-skipped. Added a custom
|
||||
`HttpAuthenticationMechanism` + `IdentityProvider` in
|
||||
`app/proprietary/.../security/identity/` (`JwtBearerAuthenticationMechanism`,
|
||||
`JwtTokenIdentityProvider`): extract `Authorization: Bearer`, validate via the existing
|
||||
`JwtService` (jjwt + keystore), build a `QuarkusSecurityIdentity` and map the `role` claim
|
||||
(`ROLE_ADMIN` → also add `ADMIN` so `@RolesAllowed("ADMIN")` matches). Returns no identity when
|
||||
no Bearer is present, so the X-API-KEY / login-off open-endpoint path is unaffected. **This is the
|
||||
IdentityProvider that ~10 `// TODO: Migration required` comments across the security/storage code
|
||||
asked for.** Run with `V2=true`.
|
||||
2. **No admin user was ever created** → all logins failed "No user found: admin". `InitialSecuritySetup`
|
||||
was a Spring `@Component` (eagerly constructed, `@PostConstruct` ran every boot); the migration
|
||||
made it a lazy `@ApplicationScoped` whose `@PostConstruct` never ran. Restored eager init via
|
||||
`@Observes StartupEvent`. **Pattern: any migrated `@PostConstruct`-on-`@ApplicationScoped` startup
|
||||
bean with no injector is dead code — grep for them.**
|
||||
3. **Eager init then exposed two latent bugs** (both real, both now fixed):
|
||||
- `@Produces @ApplicationScoped DataSource` → Arc generated the client proxy in the JDK-sealed
|
||||
`javax.sql` package → `NoClassDefFoundError` on first use. Fix: `@Singleton` (pseudo-scope, no
|
||||
proxy). **Audit other `@Produces @ApplicationScoped` whose return type is a `java.*`/`javax.*`
|
||||
type.**
|
||||
- Panache `persist()` in the `StartupEvent` observer ran with no transaction (Spring Data wrapped
|
||||
`save()` implicitly). Fix: `@Transactional` on the observer.
|
||||
4. **Login returned 500 instead of 401** for unknown user / bad password. `CustomUserDetailsService`
|
||||
threw `IllegalArgumentException`, but `AuthController` catches the migration shim
|
||||
`stirling.software.common.security.UsernameNotFoundException`. Made the service throw the shim
|
||||
type. **Sibling: the locked-account path still throws `IllegalStateException` — wire it similarly
|
||||
when needed.**
|
||||
5. **`@Transactional` missing on policy-store reads** (`JpaPolicyStore.all()`,
|
||||
`findByTriggerType()`) → the scheduled folder-watch/schedule triggers threw
|
||||
`ContextNotActiveException` off-request (§6.C). The reads are reached via the CDI proxy so a
|
||||
method-level `@Transactional` applies even from the background virtual-thread executor.
|
||||
6. **Split scenarios sent a duplicate `fileInput` text part** (`| fileInput | fileInput |` in
|
||||
`general.feature`) alongside the file part; Quarkus `@RestForm FileUpload` bound the *text* part
|
||||
("fileInput", 9 bytes) → "PDF corrupted". Spring ignored the stray part. Removed the redundant
|
||||
rows (the file is already attached via the generate step). **Real clients send one part, so this
|
||||
is a test artifact, not a server tolerance gap worth chasing.**
|
||||
7. **e2e Docker build is now first-class:** `docker/quarkus/Dockerfile` (+ `README.md`,
|
||||
`build-and-run.sh`) layers the runner-jar on the base image, and `.dockerignore` re-includes
|
||||
`app/core/build/*-runner.jar` (it was excluded by `**/build/`, so a clean `docker build` had been
|
||||
silently relying on BuildKit cache).
|
||||
|
||||
### Session 1
|
||||
|
||||
1. **`MultipartFile.transferTo` didn't overwrite** (`a30d524ec`).
|
||||
`app/common/.../model/MultipartFile.java` + `.../model/multipart/FileUploadMultipartFile.java`
|
||||
used `Files.copy(in, dest)` without `REPLACE_EXISTING`. Callers do
|
||||
`Files.createTempFile(...)` (creates the file) then `transferTo(thatPath)` → `FileAlreadyExistsException`.
|
||||
Spring's `transferTo` overwrites. **Fixed** by adding `StandardCopyOption.REPLACE_EXISTING`.
|
||||
Fixes the whole class of `/api/v1/misc/*` failures (scanner-effect, replace-invert, ocr,
|
||||
update-metadata, unlock-pdf-forms, repair, extract-image-scans, add-page-numbers, …).
|
||||
|
||||
2. **`maxDPI` defaulted to 0** (`a30d524ec`).
|
||||
`ApplicationProperties.System.maxDPI` is a primitive `int` (→ 0 when not bound from settings).
|
||||
Every DPI guard (`dpi > maxDPI`) then failed with *"maximum safe limit of 0"*. The
|
||||
`settings.yml.template` default is 500. **Fixed** by `private int maxDPI = 500;`.
|
||||
⚠️ Root cause hint: this strongly suggests **settings.yml → ApplicationProperties config binding
|
||||
is incomplete in the Quarkus migration**. Other primitive/unset fields may also be silently
|
||||
wrong. Worth a dedicated audit (see §5).
|
||||
|
||||
3. **Request-path `HttpServletRequest` → `UT000048` "No request is currently active"** (`860bd6e63`,
|
||||
`4b572852c`). This was the dominant blocker. `quarkus-rest` (RESTEasy Reactive) runs handlers on
|
||||
reactive/worker threads where the undertow servlet request context is NOT active, so ANY
|
||||
`HttpServletRequest.getX()` throws. Fixed in:
|
||||
- `GlobalExceptionHandler` (an `ExceptionMapper` that threw while handling *every* error, masking
|
||||
the real cause) → `@Context UriInfo` + exception-safe `requestUri()`.
|
||||
- `ControllerAuditAspect`, `AuditAspect` → route through the already-guarded
|
||||
`AuditService.getCurrentRequest()` (returns null off-request) + a guarded `safeResponse()`.
|
||||
- `AutoJobAspect`, `JobExecutorService` → inject `io.quarkus.vertx.http.runtime.CurrentVertxRequest`,
|
||||
read query-param/method/path/attributes from the Vert.x request, degrade to null/no-op.
|
||||
- `AuthController`, `UserController`, `ConfigController` → `@Context UriInfo` / `HttpHeaders` /
|
||||
`io.vertx.core.http.HttpServerRequest`.
|
||||
This unblocked the entire `@AutoJobPostMapping` chain (most PDF endpoints).
|
||||
|
||||
4. **License singleton PK race** (`860bd6e63`). `UserLicenseSettings` has a manually-assigned
|
||||
`@Id = 1L`. Spring Data `save()` on a non-new (pre-set-id) entity does a **MERGE (upsert)**; the
|
||||
migration converted it to `persist()` (INSERT-only). The startup license sync raced the first
|
||||
request, both inserted id=1 → `JdbcSQLIntegrityConstraintViolationException` → app crash. **Fixed**
|
||||
in `UserLicenseSettingsService.getOrCreateSettings()` with a JVM lock +
|
||||
`io.quarkus.narayana.jta.QuarkusTransaction.requiringNew()` create-once, then reload into the
|
||||
caller's tx. **⚠️ This `save()`→`persist()`-should-be-`merge()` bug almost certainly exists for
|
||||
OTHER manually-`@Id`'d entities — audit them (see §5).**
|
||||
|
||||
5. **App couldn't boot without Redis** (`4665cceeb`).
|
||||
- `quarkus.oidc.enabled=false` default (quarkus-oidc aborts startup without `auth-server-url`;
|
||||
re-enable for an OAuth2 deployment).
|
||||
- Valkey backplane beans eagerly injected the inactive `RedisDataSource`. Gated all 7 with
|
||||
**build-time** `@io.quarkus.arc.properties.IfBuildProperty(name="cluster.backplane", stringValue="valkey")`
|
||||
(NOT `@LookupIfProperty` — that leaves the bean in the build, so `RedisDataSource` still has a
|
||||
consumer and Quarkus emits an eager startup observer that fails). Plus
|
||||
`quarkus.redis.health.enabled=false`.
|
||||
|
||||
6. **Runtime boot fixes** (`20b25ad76`): `quarkus.hibernate-orm.mapping.format.global=ignore` (JSON
|
||||
columns), Quartz cron `0 0 0 * * MON` → `0 0 0 ? * MON` (Quartz rejects `*` in both day fields),
|
||||
`@Scheduled(every="7d")` → `"P7D"`, `quarkus.arc.fail-on-intercepted-private-method=false`.
|
||||
|
||||
7. **CDI augmentation** (`185ac88b3`): interceptor bindings made `@InterceptorBinding`
|
||||
(`@EnterpriseEndpoint`, `@PremiumEndpoint`), a `tools.jackson.databind.ObjectMapper` producer
|
||||
added in `AppConfig` (92 injection points), ambiguous beans resolved (`@DefaultBean`),
|
||||
`Optional<X>`→`Instance<X>`, collection `List<X>`→`@All List<X>`, nested `SAML2` config producer.
|
||||
|
||||
8. **Test layer** (`d51228af6`): a content-based exclude in root `build.gradle subprojects` skips any
|
||||
test still importing `org.springframework`/`com.nimbusds` (self-maintaining), plus an explicit
|
||||
list for tests asserting changed production signatures.
|
||||
|
||||
---
|
||||
|
||||
## 5. Recurring patterns / gotchas (apply these everywhere)
|
||||
|
||||
- **HttpServletRequest is poison on reactive threads.** ~35 main-source files still reference
|
||||
`HttpServletRequest` (see §6 list). For each in the request path, replace with:
|
||||
- path/URI → `@Context jakarta.ws.rs.core.UriInfo` (`uriInfo.getRequestUri().getPath()`), or in a
|
||||
non-JAX-RS bean inject `io.quarkus.vertx.http.runtime.CurrentVertxRequest`
|
||||
(`currentVertxRequest.getCurrent().request().path()`), guarded in try/catch returning null/"".
|
||||
- headers → `@Context jakarta.ws.rs.core.HttpHeaders` (`getHeaderString(name)`).
|
||||
- remote addr / method → `@Context io.vertx.core.http.HttpServerRequest`.
|
||||
- request attributes (`get/setAttribute`) → Vert.x `RoutingContext.get/put` via `CurrentVertxRequest`.
|
||||
- In services that already have a guarded accessor, reuse `AuditService.getCurrentRequest()`.
|
||||
- **Spring `save()` → Panache:** if the entity uses `@GeneratedValue` (new on insert) → `persist()`.
|
||||
If the entity has a **manually-assigned `@Id`** (caller sets the id, "upsert" semantics) →
|
||||
`getEntityManager().merge()` (NOT `persist()`), and consider concurrency.
|
||||
- **Config gating:** runtime selection that must REMOVE a bean (so its deps don't get wired) →
|
||||
build-time `@IfBuildProperty`/`@UnlessBuildProperty`. `@LookupIfProperty` only disables *lookup*,
|
||||
the bean and its injection points stay in the build.
|
||||
- **`quarkus.*` build-time props** (e.g. `quarkus.oidc.enabled`, `quarkus.hibernate-orm.*`,
|
||||
`quarkus.arc.*`) can't be overridden by env at runtime — they require a rebuild.
|
||||
- **settings.yml binding is suspect** (see maxDPI). Audit `ApplicationProperties` for primitive
|
||||
fields that need non-zero/template defaults, and verify the settings.yml → ApplicationProperties
|
||||
binding path actually works in Quarkus (it was Spring `@ConfigurationProperties` + a custom YAML
|
||||
property source — see the `YamlPropertySourceFactory` / `ConfigInitializer` TODOs).
|
||||
- **Augment gate:** `compileJava` passing ≠ working. `./gradlew :stirling-pdf:quarkusBuild` surfaces
|
||||
CDI wiring errors; only *running* surfaces the `UT000048` / config / race bugs. Always run.
|
||||
- **Jackson 2 vs 3 coexist:** ~100 files use `tools.jackson` (Jackson 3, from Spring Boot 4); REST
|
||||
(de)serialization uses Quarkus' Jackson 2. Don't "fix" `tools.jackson` imports — there's a producer.
|
||||
|
||||
---
|
||||
|
||||
## 6. REMAINING WORK (prioritized)
|
||||
|
||||
### A. Make the e2e Docker build first-class
|
||||
- [x] **DONE (Session 2):** `docker/quarkus/Dockerfile` (+ `README.md`, `build-and-run.sh`) committed,
|
||||
uses the runner-jar, copies fonts; `.dockerignore` re-includes `app/core/build/*-runner.jar`.
|
||||
- [ ] Rewrite/replace `docker/embedded/Dockerfile`, `Dockerfile.fat`, `Dockerfile.ultra-lite` for
|
||||
Quarkus: drop the Spring-Boot `-Djarmode=tools extract --layers` + `spring-boot-loader` layer
|
||||
copies; either copy the uber `-runner.jar` to `/app/app.jar` or use the Quarkus fast-jar
|
||||
(`quarkus-app/`) layout. The stage-1 `gradle clean build -PbuildWithFrontend=true` still builds
|
||||
the frontend (fine).
|
||||
- [ ] Update `scripts/init.sh` / `init-without-ocr.sh` — they have Spring-loader fallbacks and AOT
|
||||
machinery; the primary `java -jar /app.jar` path works for the uber-jar, but verify the AOT
|
||||
cache + `restart-helper.jar` paths.
|
||||
- [ ] Then `testing/test.sh` (the official cucumber driver) should work end-to-end.
|
||||
|
||||
### B. Real per-endpoint bugs surfaced by cucumber (login-off suite)
|
||||
Last measured failure buckets (before the transferTo/maxDPI fixes — re-run to refresh):
|
||||
- [ ] **`PdfCorruptedException` (~48)** on `convert/pdf/{word,vector,presentation,text,pdfa,...}`,
|
||||
`convert/{html,cbz}/pdf`. Investigate `CustomPDFDocumentFactory` (PDF loading) — is it
|
||||
misreporting valid PDFs as corrupted, or do these convert paths need LibreOffice/handling that
|
||||
errors first and gets wrapped? Check one: `python -m behave features/convert_new.feature:NN --format plain`
|
||||
then read `docker logs sp-e2e` for the real cause.
|
||||
- [ ] **`ClassCastException: String cannot be cast to ...` (~6)** — form/param binding type mismatch.
|
||||
Likely a `@RestForm`/`@QueryParam` bound to the wrong type, or a Map/JSON form field. Check
|
||||
`form/fill`, `form_advanced.feature`.
|
||||
- [ ] **Remaining `500`s** after A/B fixes — `misc/compress-pdf`, `general/split-pdf-by-chapters`,
|
||||
`misc/add-image`, etc. Triage each via container logs.
|
||||
- [ ] **`400`s (~5)** — multipart `@RestForm` binding gaps. The migration left several request DTOs
|
||||
with `MultipartFile`/POJO-list fields not bound to RESTEasy `FileUpload` (AI/workflow/sign DTOs
|
||||
explicitly flagged). See `migration-report.md` "Representative deferred code".
|
||||
- [ ] **temp-file collisions other than transferTo** — also check `GeneralUtils.createTempFile`
|
||||
(`app/common/.../util/GeneralUtils.java:79/85`) and any `Files.createFile`/`Files.copy`/
|
||||
`Files.move` without `REPLACE_EXISTING`. `temp<rand>genericNonCustomisableName.pdf` and
|
||||
`/tmp/stirling-pdf/stirling-pdf-<rand>.pdf` were two such names.
|
||||
|
||||
### C. Background scheduled-task errors (log noise, not request-breaking)
|
||||
- [ ] `FolderWatchTrigger` (reconcile) and `ScheduleTrigger` (sweep) throw
|
||||
`jakarta.enterprise.context.ContextNotActiveException` ("neither a transaction nor a CDI
|
||||
request context is active") because they hit Panache/`PolicyRepository` off-request. Add
|
||||
`@Transactional` (and/or `@ActivateRequestContext`) to those scheduled methods, or wrap the EM
|
||||
access in `QuarkusTransaction.requiringNew()`. Files:
|
||||
`app/proprietary/.../policy/trigger/FolderWatchTrigger.java`,
|
||||
`.../policy/trigger/ScheduleTrigger.java`, `.../policy/store/JpaPolicyStore.java`.
|
||||
|
||||
### D. The remaining ~35 `HttpServletRequest` files (apply §5 pattern as they surface)
|
||||
Not all are in the hot path; fix the ones that throw `UT000048` when their endpoints are exercised.
|
||||
Get the list any time with:
|
||||
```bash
|
||||
grep -rln "HttpServletRequest" app/core/src/main app/proprietary/src/main app/common/src/main --include=*.java
|
||||
```
|
||||
Known-fixed already: GlobalExceptionHandler, ControllerAuditAspect, AuditAspect, AutoJobAspect,
|
||||
JobExecutorService, AuthController, UserController, ConfigController. Everything else is unverified.
|
||||
High-risk: security filters (`UserAuthenticationFilter`, rate-limit filters, `JwtAuthenticationFilter`),
|
||||
anything reading headers/cookies/remote-addr per request.
|
||||
|
||||
### E. Auth / JWT / login — DONE (Session 2)
|
||||
The whole Quarkus auth-identity layer is now in place (`app/proprietary/.../security/identity/`):
|
||||
- [x] **JWT Bearer** → `JwtBearerAuthenticationMechanism` + `JwtTokenIdentityProvider` (validate via
|
||||
`JwtService`, map `role` claim). Run with `V2=true`.
|
||||
- [x] **X-API-KEY** → `ApiKeyAuthenticationMechanism` + `ApiKeyAuthenticationRequest` +
|
||||
`ApiKeyIdentityProvider` (resolve via `userService.getUserByApiKey`). Lets `X-API-KEY` requests
|
||||
authenticate (e.g. `/me`), and lets the suite run `SECURITY_ENABLELOGIN=true`.
|
||||
- [x] **User-as-principal** → `UserSecurityIdentityAugmentor` re-loads the `User` and sets it as the
|
||||
`SecurityIdentity` principal; `User implements Principal`. This satisfies the ~7
|
||||
`principal instanceof User` sites (folders, file storage, sessions, audit, UserController) — the
|
||||
augmentor every `// TODO: Migration required` in security/storage asked for.
|
||||
- [x] **Config binding** → `ApplicationPropertiesConfigOverlay` overlays env/config onto
|
||||
`ApplicationProperties` at startup (the Spring `@ConfigurationProperties` bind was never
|
||||
migrated, so `SECURITY_ENABLELOGIN` / `SECURITY_CUSTOMGLOBALAPIKEY` / `STORAGE_ENABLED` were
|
||||
ignored — root cause of the maxDPI/loginAttemptCount class too). **Currently a focused subset
|
||||
(auth/storage/SSO toggles); a complete generic bind (all ~445 fields + settings.yml) is still
|
||||
TODO.**
|
||||
- Validated on a login-ON probe (`SECURITY_ENABLELOGIN=true V2=true STORAGE_ENABLED=true
|
||||
SECURITY_CUSTOMGLOBALAPIKEY=123456789`): open PDF endpoints (anon), JWT login+/me, X-API-KEY /me,
|
||||
folder list/create all work. The 183 open endpoints stay open (no global `quarkus.http.auth.*`
|
||||
policy), so login-ON does not regress them.
|
||||
|
||||
### F. SAML / SSO — DONE (Session 2), both flows work end-to-end
|
||||
|
||||
**Both `validate-oauth-test.sh` and `validate-saml-test.sh` pass, and both full login flows were
|
||||
verified end-to-end against the Keycloak compose** (login -> IdP -> callback/ACS -> auto-created
|
||||
user -> app JWT cookie -> `/me` 200). Run with `PREMIUM_KEY=<your enterprise license key>`. Tag the Quarkus image as
|
||||
`docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest` so the compose uses it (or repoint the
|
||||
`image:`); `start-saml-test.sh` generates the SP certs + fetches Keycloak's cert.
|
||||
|
||||
- **OAuth2 / OIDC** (`security/oauth2/`): `OAuth2LoginController` (JAX-RS) serves
|
||||
`/oauth2/authorization/{id}` -> IdP authorize redirect; `OAuth2CallbackServlet` (`@WebServlet
|
||||
/login/oauth2/code/*`) does the code exchange + userinfo + auto-create + JWT cookie. **Why a
|
||||
servlet for the callback:** quarkus-undertow's default servlet owns the `/login/*` prefix and
|
||||
query-strips/intercepts the extension-less callback before RESTEasy sees it; a registered
|
||||
`@WebServlet` takes precedence. (Same reason the SAML SP endpoints are servlets.)
|
||||
- **SAML2** (`security/saml2/`): `Saml2Service` (OpenSAML 5) initialises the library, loads SP
|
||||
key/cert + IdP cert, builds SP metadata, builds+signs the redirect-binding AuthnRequest, and
|
||||
validates the SAMLResponse signature (`SAMLSignatureProfileValidator` + `SignatureValidator`
|
||||
against the IdP cert). `SamlMetadataServlet` -> `/saml2/service-provider-metadata/{id}`;
|
||||
`SamlSpServlet` -> `/saml2/authenticate/{id}` (login init) + `/login/saml2/sso/{id}` (ACS).
|
||||
**Gotcha:** the SP entityId must equal the SP-metadata URL (`{backendUrl}/saml2/service-provider-
|
||||
metadata/{id}`), which is what Keycloak's SAML client is keyed on - NOT the bare
|
||||
`SECURITY_SAML2_SP_ENTITYID` host (`SamlConfig` derives it). Keycloak's realm has
|
||||
`saml.client.signature=false` (AuthnRequest signature optional) + `saml.server.signature=true`
|
||||
(so the ACS validates the response against Keycloak's cert).
|
||||
- Both flows finish by issuing the app JWT as the `stirling_jwt` cookie, which
|
||||
`JwtBearerAuthenticationMechanism` now also reads (not just the `Authorization` header) -> feeds
|
||||
the `UserSecurityIdentityAugmentor` (principal = User).
|
||||
- Follow-ups: logout/SLO endpoints; encrypted-assertion handling; the `mcp` Keycloak compose;
|
||||
desktop/Tauri RelayState (`TauriSamlUtils` preserved). Multi-provider (google/github) OAuth uses
|
||||
the same pattern keyed by registrationId.
|
||||
|
||||
### F-old. (superseded) original SAML/SSO scoping
|
||||
The `validate-*-test.sh` scripts are **endpoint-existence
|
||||
checks** (Keycloak up + Stirling serves the SSO endpoint), not full browser logins.
|
||||
|
||||
Prereqs for any run: the compose files use `image: docker.stirlingpdf.com/.../stirling-pdf:latest`
|
||||
(the published Spring image) — **repoint to `stirling-pdf-quarkus:jwt`** (or wire
|
||||
`docker/quarkus/Dockerfile`). The SAML compose mounts `saml-private-key.key`/`saml-public-cert.crt`/
|
||||
`keycloak-saml-cert.pem` which **do not exist in the repo** — generate them (the SP signing
|
||||
key/cert; `start-saml-test.sh` may do this). SAML/OAuth need the **Enterprise license** env.
|
||||
|
||||
**OAuth2 / OIDC** (more tractable — Quarkus has `quarkus-oidc`):
|
||||
- [ ] Extend `ApplicationPropertiesConfigOverlay` for `security.oauth2.*` (client issuer/clientId/
|
||||
clientSecret/scopes/useAsUsername) — currently only the `enabled` toggle is bound.
|
||||
- [ ] Serve `GET /oauth2/authorization/{registrationId}` → 302 to the IdP authorize URL (login
|
||||
initiation; the OAuth `validate` script checks this responds). Build from issuer + clientId +
|
||||
redirect-uri `/login/oauth2/code/{registrationId}`.
|
||||
- [ ] Serve the callback `GET /login/oauth2/code/{registrationId}` → exchange code (REST Client to
|
||||
the token endpoint), fetch userinfo, auto-create/login the user (reuse `CustomOAuth2UserService`
|
||||
logic), issue the app JWT via `JwtService`. `quarkus.oidc.enabled` is **build-time** and aborts
|
||||
startup with no `auth-server-url`, so either hand-roll the flow (simplest, no build-time gate)
|
||||
or enable oidc with a runtime-disabled default tenant.
|
||||
|
||||
**SAML2** (larger — no Quarkus SAML extension; OpenSAML 5 from scratch):
|
||||
- [ ] `Saml2Configuration` already loads the SP/IdP certs and computes entityId/ACS/SLO URLs and
|
||||
customizes the AuthnRequest (all preserved). Build on it:
|
||||
- [ ] `GET /saml2/service-provider-metadata/{registrationId}` → SP `EntityDescriptor` XML
|
||||
(ACS=`/login/saml2/sso/{id}`, SP signing cert) marshalled via OpenSAML 5. (The SAML `validate`
|
||||
script checks this.)
|
||||
- [ ] login initiation → build+sign an `AuthnRequest` (use `customizeAuthnRequest`) and
|
||||
redirect/POST to `samlConf.getIdpSingleLoginUrl()`.
|
||||
- [ ] `POST /login/saml2/sso/{registrationId}` (ACS) → validate the SAML response/assertion against
|
||||
the IdP cert, extract the NameID/attributes, auto-create/login the user, issue the app JWT.
|
||||
- Host these as Jakarta `@WebServlet` (quarkus-undertow) or JAX-RS resources; gate on
|
||||
`security.saml2.enabled`.
|
||||
- [ ] Both flows then feed the existing `UserSecurityIdentityAugmentor` (principal=User) once they
|
||||
establish the session/JWT.
|
||||
|
||||
### G. `saas` flavor full augmentation (optional, non-default)
|
||||
- [ ] `STIRLING_FLAVOR=saas ./gradlew :stirling-pdf:quarkusBuild` → ~28 Arc deployment problems
|
||||
(Supabase second datasource via `quarkus.datasource."supabase".*`, `SecurityFilterChain`/
|
||||
`JwtDecoder` → `quarkus.http.auth.*`+OIDC, credit `HandlerInterceptor`/`@RestControllerAdvice`
|
||||
→ JAX-RS `@Provider`/`ExceptionMapper`, `@ConfigurationProperties` → `@ConfigMapping`,
|
||||
RestTemplate → REST Client). ~90 `// TODO: Migration required` across 34 saas files.
|
||||
|
||||
### H. Test suite (unit/integration) re-enablement
|
||||
- [ ] ~180 test files are excluded from compilation (content filter on `org.springframework`/
|
||||
`com.nimbusds` imports + an explicit list in `build.gradle`). Port them to `@QuarkusTest`
|
||||
incrementally; as a file's Spring imports go away it auto-re-enters the build.
|
||||
|
||||
### I. Loose ends
|
||||
- [ ] `/q/openapi` returns 500 (`UT000048`) — known quarkus-undertow + smallrye-openapi interaction;
|
||||
swagger-ui works, live API works. Affects API-doc tooling only.
|
||||
- [ ] Jackson 2/3 convergence (drop `tools.jackson`).
|
||||
- [ ] ~437 `// TODO: Migration required` markers across the codebase document every deferred decision;
|
||||
`grep -rn "TODO: Migration required" app/*/src/main` to enumerate.
|
||||
|
||||
---
|
||||
|
||||
## 7. Quick reference — env vars used in e2e
|
||||
|
||||
| Var | Value | Why |
|
||||
|-----|-------|-----|
|
||||
| `SECURITY_ENABLELOGIN` | `false` | run without auth (most API tests); set `true` for the JWT suite |
|
||||
| `METRICS_ENABLED` | `true` | enables `/api/v1/info/*` (info.feature) |
|
||||
| `SYSTEM_DEFAULTLOCALE` | `en-US` | matches default-language change |
|
||||
| `SYSTEM_MAXFILESIZE` | `100` | upload limit for tests |
|
||||
| `QUARKUS_HTTP_PORT` | `8080` | cucumber steps hardcode 8080 |
|
||||
| `QUARKUS_DATASOURCE_JDBC_URL` | `jdbc:h2:mem:...` | use a fresh in-mem DB for clean runs (avoids stale H2 file lock) |
|
||||
|
||||
Default datasource (in `application.properties`) is **H2 file** at
|
||||
`./configs/stirling-pdf-DB-2.3.232` — fine in a container; for repeated host runs override to
|
||||
`jdbc:h2:mem:...` to dodge the file lock (`Database may be already in use`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Useful diagnostic one-liners
|
||||
|
||||
```bash
|
||||
# container alive + real error (strip ANSI, drop known background noise)
|
||||
docker logs sp-e2e 2>&1 | sed 's/\x1b\[[0-9;]*m//g' \
|
||||
| grep -iE "ERROR|Caused by|Exception" \
|
||||
| grep -viE "Log4j|LogManager|ForkJoinPool|FolderWatch|ScheduleTrigger|policy-" | tail -30
|
||||
|
||||
# categorize cucumber failures
|
||||
cd testing/cucumber && TEST_CONTAINER_NAME=sp-e2e python -m behave --no-capture --format plain --no-skipped > /tmp/behave.txt 2>&1
|
||||
grep -oE "Expected status code [0-9]+ but got [0-9]+" /tmp/behave.txt | sort | uniq -c | sort -rn
|
||||
grep -oE "features/[a-z_]+\.feature" /tmp/behave.txt | sort | uniq -c | sort -rn # rough; use a junit reporter for precise
|
||||
|
||||
# what still touches the servlet request
|
||||
grep -rln "HttpServletRequest" app/*/src/main --include=*.java
|
||||
|
||||
# enumerate deferred work
|
||||
grep -rn "TODO: Migration required" app/*/src/main --include=*.java | wc -l
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Measured cucumber results — newest first
|
||||
|
||||
**Run 5 — LOGIN ON (`SECURITY_ENABLELOGIN=true V2=true STORAGE_ENABLED=true
|
||||
SECURITY_CUSTOMGLOBALAPIKEY=123456789`):**
|
||||
```
|
||||
18 features passed, 7 failed, 0 skipped
|
||||
304 scenarios passed, 34 failed, 0 skipped <-- folders + user-scoped features now pass
|
||||
```
|
||||
X-API-KEY mechanism + User-principal augmentor + config overlay made login-ON work without
|
||||
regressing the open endpoints (0 folder failures). Trajectory: **183 → 223 → 272 → 291 → 304**.
|
||||
|
||||
**Run 4 — login off + lockout fix:** `291 passed, 47 failed, 0 skipped`.
|
||||
|
||||
**Run 3 — login off + `V2=true` + JWT Bearer mechanism (Session 2):**
|
||||
```
|
||||
17 features passed, 8 failed, 0 skipped
|
||||
272 scenarios passed, 66 failed, 0 skipped <-- 0 skipped: all JWT/admin scenarios now run
|
||||
```
|
||||
The JWT mechanism unskipped all 80 and added +49 passing over run 2 with no regressions. Remaining
|
||||
66 failures, biggest buckets:
|
||||
- **~38 = login-lockout cascade (FIXED, pending re-measure).** `loginAttemptCount` defaulted to 0
|
||||
(template = 5) → admin locked after one failed-login test → every later admin scenario blocked
|
||||
("Admin login failed" 21×, "Folder list returned" 17×). Fixed the primitive default (same as
|
||||
maxDPI). **Re-run to confirm; expect ~300+.**
|
||||
- 10×(200→403) feature-gated/disabled (mostly not bugs).
|
||||
- 5×(200→401) + 2×(401→403) — auth scenarios asserting specific codes; triage individually.
|
||||
- 3×(200→500) — real per-endpoint bugs (e.g. `user/get-api-key`). Triage via container logs.
|
||||
|
||||
**Run 2 — login off, Session 2 fixes, no JWT mechanism:**
|
||||
```
|
||||
16 features passed, 5 failed, 4 skipped
|
||||
223 scenarios passed, 35 failed, 80 skipped
|
||||
```
|
||||
|
||||
**Run 1 — original baseline (login off):**
|
||||
```
|
||||
183 scenarios passed, 75 failed, 80 skipped
|
||||
```
|
||||
|
||||
Trajectory this session: **183 → 223 (boot/login/test fixes) → 272 (JWT mechanism), 0 skipped.**
|
||||
@@ -0,0 +1,632 @@
|
||||
# Quarkus migration TODO
|
||||
|
||||
Backlog for finishing the Spring Boot -> Quarkus port of this branch. The per-file
|
||||
`TODO: Migration required` comments that used to carry this information were removed from the
|
||||
source and folded in here. Companion docs: `QUARKUS_MIGRATION_HANDOFF.md` (stack, commands,
|
||||
patterns) and `migration-report.md` (the original summary).
|
||||
|
||||
## Status
|
||||
|
||||
| Module | main sources | test sources | notes |
|
||||
|---|---|---|---|
|
||||
| `:common` | compiles | compiles | no Spring imports |
|
||||
| `:stirling-pdf` (`app/core`) | compiles | compiles | no Spring imports |
|
||||
| `:proprietary` | **compiles** | **fails** | see [Test layer](#test-layer) |
|
||||
| `:saas` | not measured | not measured | opt-in flavor, 21 files still on Spring |
|
||||
|
||||
The **proprietary (default) flavor builds, boots and serves**:
|
||||
|
||||
```bash
|
||||
STIRLING_FLAVOR=proprietary ./gradlew :stirling-pdf:quarkusBuild -PnoSpotless
|
||||
```
|
||||
|
||||
```bash
|
||||
java -jar app/core/build/stirling-pdf-*-runner.jar
|
||||
```
|
||||
|
||||
Verified on that jar: Quarkus augmentation clean, **0 errors during startup**, and
|
||||
`rotate-pdf`, `get-info-on-pdf` and `compress-pdf` all return valid output over HTTP.
|
||||
Its OpenAPI document carries **333 operations over 315 paths**, against main's 294/277.
|
||||
|
||||
`./gradlew build` still fails, because `:proprietary` test sources do not compile yet - and note
|
||||
`settings.gradle` includes `:proprietary` on *every* flavor, so that blocks the `core` leg too.
|
||||
|
||||
## Test layer
|
||||
|
||||
`STIRLING_FLAVOR=proprietary ./gradlew :proprietary:compileTestJava` fails in 16 files.
|
||||
The main-source port moved signatures the tests still assert against. The three shapes:
|
||||
|
||||
- **`findById` stubs.** Spring Data returned `Optional<E>`; Panache's inherited `findById` returns
|
||||
a nullable entity and `findByIdOptional` returns the `Optional`. Production call sites moved to
|
||||
`findByIdOptional`, so `when(repo.findById(id)).thenReturn(Optional.of(x))` has to follow.
|
||||
- **A missing `save()` shim, which is a production bug, not a test bug.**
|
||||
`WorkflowParticipantRepository` and `FileEncryptionKeyRepository` are Panache repositories that
|
||||
never regained the `save(E)` that main's `JpaRepository` supplied for free. Add the shim rather
|
||||
than editing the tests around it.
|
||||
- **`Environment` mocks.** `TeamMembershipService` moved to MicroProfile Config;
|
||||
`TeamMembershipServiceTest` still mocks Spring's `Environment` - and does so *fully qualified*,
|
||||
so the import-based test exclusion filter does not catch it.
|
||||
|
||||
- [ ] `proprietary/accountlink/InstanceEntitlementGateTest.java` - 2 distinct error(s)
|
||||
- [ ] `proprietary/controller/api/PdfCommentAgentControllerTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/controller/api/ProprietaryUIDataControllerTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/policy/config/FolderAccessGuardTest.java` - 2 distinct error(s)
|
||||
- [ ] `proprietary/policy/engine/PolicyExecutorTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/policy/input/FolderInputSourceTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/policy/output/FolderOutputSinkTest.java` - 2 distinct error(s)
|
||||
- [ ] `proprietary/policy/s3/EmbeddedS3CredentialMigrationTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/policy/source/JpaSourceStoreTest.java` - 2 distinct error(s)
|
||||
- [ ] `proprietary/security/controller/api/UserControllerTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/security/service/ApiKeyAuthenticationServiceTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/security/service/TeamMembershipServiceTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/security/service/UserServiceTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/service/AiWorkflowServiceTest.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/storage/crypto/InMemoryKeyRepo.java` - 1 distinct error(s)
|
||||
- [ ] `proprietary/workflow/service/WorkflowSessionServiceTest.java` - 1 distinct error(s)
|
||||
|
||||
## `:saas` flavor
|
||||
|
||||
21 files still import Spring. Same three tiers as the proprietary port, so the same
|
||||
recipes apply. `:saas` sits on `:proprietary`, so it could not be measured until now.
|
||||
|
||||
### Spring Data repositories -> Panache (5)
|
||||
|
||||
- [ ] `saas/accountlink/LinkedInstanceRepository.java`
|
||||
- [ ] `saas/payg/bundle/PrepaidBundleRepository.java`
|
||||
- [ ] `saas/payg/repository/PaygInstanceUsageRepository.java`
|
||||
- [ ] `saas/procurement/repository/ProcurementDealRepository.java`
|
||||
- [ ] `saas/procurement/repository/ProcurementQuoteRepository.java`
|
||||
|
||||
### Services and config (9)
|
||||
|
||||
- [ ] `saas/accountlink/AccountLinkService.java`
|
||||
- [ ] `saas/accountlink/LinkedInstanceAuthenticationToken.java`
|
||||
- [ ] `saas/model/SaasUserExtensions.java`
|
||||
- [ ] `saas/payg/instance/InstanceUsageIngestService.java`
|
||||
- [ ] `saas/payg/stripe/StripeInvoiceDao.java`
|
||||
- [ ] `saas/payg/stripe/StripePaymentMethodDao.java`
|
||||
- [ ] `saas/procurement/license/KeygenEnterpriseLicenseService.java`
|
||||
- [ ] `saas/procurement/license/MockEnterpriseLicenseService.java`
|
||||
- [ ] `saas/security/SaasPortalAuditScopeResolver.java`
|
||||
|
||||
### REST controllers and filters (7)
|
||||
|
||||
- [ ] `saas/accountlink/AccountLinkController.java`
|
||||
- [ ] `saas/accountlink/DeviceCredentialAuthenticationFilter.java`
|
||||
- [ ] `saas/accountlink/InstanceController.java`
|
||||
- [ ] `saas/payg/api/PaygInvoicesController.java`
|
||||
- [ ] `saas/payg/api/PaygPaymentMethodController.java`
|
||||
- [ ] `saas/procurement/api/ProcurementController.java`
|
||||
- [ ] `saas/usage/SaasFleetUsageController.java`
|
||||
|
||||
## Parity gaps against main
|
||||
|
||||
Measured by diffing the OpenAPI document of a booted `origin/main` (Spring, proprietary flavor)
|
||||
against a booted branch jar of the same flavor.
|
||||
|
||||
- **The automation/policy stack is unavailable on the proprietary flavor.** Every policy bean
|
||||
carries `@IfBuildProfile("saas")` - a pre-existing branch decision, not something main does:
|
||||
main serves policies on proprietary. Arc turns that gate into `@Vetoed` off the saas profile, so
|
||||
each consumer needs the same gate or augmentation fails; 11 controllers/services were gated to
|
||||
make the flavor build. The visible symptom in the spec diff is one missing operation,
|
||||
`GET /api/v1/admin/settings/policies/implied-folder-roots`, but the whole subsystem is off.
|
||||
Deciding whether policies should run on proprietary is an owner call, not a mechanical port.
|
||||
- **`policies.streamTimeoutMs` is ignored.** `/api/v1/policies/run-stream` used Spring's
|
||||
`SseEmitter(timeout)`; JAX-RS SSE has no per-sink deadline, so the stream is now bounded by the
|
||||
container's HTTP idle timeout instead of the configured 30 minutes.
|
||||
- **Multipart parameters no longer bind from the query string.** Spring's `@RequestParam` read
|
||||
both the query string and the form body; `@RestForm` reads only the body. 20 operations are
|
||||
affected. A client that passed these as query parameters on a multipart POST would now get a
|
||||
null. The frontend sends them as form fields, so this is a compatibility narrowing for API
|
||||
callers rather than a broken feature.
|
||||
|
||||
Everything else in the spec diff is benign: 97 operations where main published an opaque request
|
||||
body and the branch now describes the individual multipart fields, 20 same-name relocations from
|
||||
the change above, and 40 branch-only routes (static/SPA paths, MCP, mobile-scanner, AI) that
|
||||
springdoc did not document. **No operation loses a parameter outright.**
|
||||
|
||||
## 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 ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/config/EndpointInspector.java:32` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:85` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/catalog/McpToolCatalog.java:113` - 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` - 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` - 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/payg/entitlement/EntitlementGuard.java:366` - 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` - 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. 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. any @Aspect/@Around advice must be rewritten as CDI interceptors.
|
||||
- `app/core/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java:11` - 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 ...
|
||||
- `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} ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java:3` - 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 ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/config/WAUTrackingFilter.java:21` - 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` - 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 ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java:152` - 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 ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:106` - 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` - 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 ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java:873` - 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` - 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 ...
|
||||
- `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 ...
|
||||
- `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. {@code AuditService}'s helper methods ({@code createBaseAuditData}, {@code ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:38` - multiple {@code @Around} advice 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java:204` - (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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpApiKeyAuthFilter.java:42` - 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` - 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 ...
|
||||
- `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. this was a Spring Security {@code OAuth2TokenValidator<Jwt>} ...
|
||||
- `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}. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpRequestSizeFilter.java:27` - 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. 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} ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:57` - @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` - 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. this was a Spring Security {@code OncePerRequestFilter} that read and rewrote the {@code SecurityContextHolder} ({@code JwtAuthenticationToken}/{@code Jwt}). Quarkus has no global mutable ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpUserBindingFilter.java:60` - 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` - 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` - 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` - 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 callers must adapt to the PanacheQuery return type (see class doc). ---------------------------------------------------------------------
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:21` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:24` - 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 / ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:89` - "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` - 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 ...
|
||||
- `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 ...
|
||||
- `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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/ProprietaryWebMvcConfig.java:10` - 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` - 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. 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 ...
|
||||
- `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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java:176` - Reusable CORS settings preserved from the original {@code corsConfigurationSource()} bean. 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} ...
|
||||
- `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. 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` - 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). 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` - 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` - 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. if this filter must run before/after other filters, ordering is not expressed by @WebFilter; configure quarkus.http.filter.* or a ServletExtension if ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:47` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:150` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java:220` - 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` - 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. ContainerRequestContext does not expose the remote address. Inject quarkus' RoutingContext ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java:40` - @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 ...
|
||||
- `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. 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. 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. 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. Spring's @Profile("!saas") gated this filter so it was NOT registered in the "saas" profile ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserBasedRateLimitingFilter.java:48` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/TauriAuthorizationRequestResolver.java:6` - 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 ...
|
||||
- `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. 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` - 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 ...
|
||||
- `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. 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.* ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/web/CorrelationIdFilter.java:22` - 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. rewrite any @Aspect advice (e.g. CreditSuccessAdvice) as CDI interceptors.
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:75` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:109` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:141` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:189` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:244` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:268` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter.
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:344` - @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` - @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` - @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` - @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` - @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` - @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` - @PreAuthorize("isAuthenticated()") complex SpEL; enforce authenticated access via JAX-RS SecurityContext / filter. 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. was a Spring {@code @RestController} with method-injected {@code Authentication} and {@code @PreAuthorize("isAuthenticated()")}. Now JAX-RS: auth comes from the {@link ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/payg/cap/AiToolRoutes.java:29` - 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` - 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 ...
|
||||
- `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. 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. 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygChargeInterceptor.java:91` - 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` - 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` - 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` - 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` - 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` - 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` - @ConfigurationProperties(prefix="payg.filter"); bind via @ConfigProperty or @ConfigMapping
|
||||
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java:33` - 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/payg/filter/PaygResponseBodyWrapperFilter.java:61` - 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. the Spring {@code WebMvcConfigurer#addInterceptors} registration was removed. Re-express it as ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:55` - Stateless JWT authentication filter for the saas profile. 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java:72` - 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` - 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` - 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` - 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` - 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` - 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. 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. 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` - 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` - --------------------------------------------------------------------------------------------- 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. 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:72` - 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:168` - 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). reimplement Saml2Configuration / CustomSaml2* on OpenSAML 5. ----
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java:54` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/controller/api/ProprietaryUIDataController.java:447` - 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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/McpServerController.java:229` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:56` - 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` - sendRedirect("/logout?userIsDisabled=true")
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:74` - sendRedirect("/login?error=locked")
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:88` - sendRedirect("/login?error=locked")
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:93` - sendRedirect("/login?error=badCredentials")
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:98` - sendRedirect("/login?error=oauth2AuthenticationError")
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationFailureHandler.java:102` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/CustomAuthenticationSuccessHandler.java:78` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java:280` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/Authority.java:20` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/exception/AuthenticationFailureException.java:3` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:54` - 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` - 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` - 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` - 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` - 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` - 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` - 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 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` - 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` - "extract username from the quarkus-oidc principal");
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:222` - 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` - "extract the 'sub' claim from the quarkus-oidc principal");
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:229` - placeholder for the saved-request redirect URL. Originally SavedRequest.getRedirectUrl().
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:233` - "resolve the saved-request redirect URL under quarkus-oidc");
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:236` - placeholder for delegating to the saved-request redirect. Originally SavedRequestAwareAuthenticationSuccessHandler.onAuthenticationSuccess(...).
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:242` - "redirect to the saved/original destination under quarkus-oidc");
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java:252` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:50` - @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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/oauth2/OAuth2Configuration.java:116` - 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` - 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` - 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` - 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` - this was a Spring Security
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CertificateUtils.java:19` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:23` - 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: - ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:83` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/CustomSaml2ResponseAuthenticationConverter.java:115` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:39` - 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` - 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` - original returned Saml2PostAuthenticationRequest.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java:112` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:41` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:75` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java:142` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/AppUpdateAuthService.java:24` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:58` - Resolves and validates the local user for an OIDC login. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/CustomOAuth2UserService.java:114` - 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` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtServiceInterface.java:17` - 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` - 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` - 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}. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java:999` - Quarkus migration: was SecurityContextHolder.getContext().getAuthentication(). 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:74` - 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` - 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` - canAccessShareLink still takes Spring Authentication; pass `securityIdentity` once FileStorageService is migrated.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java:380` - 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` - 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. 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java:84` - 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 ...
|
||||
- `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. 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 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` - 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 ...
|
||||
- `app/common/src/main/java/stirling/software/common/cluster/inprocess/LocalDiskFileStoreConfiguration.java:17` - 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 ...
|
||||
- `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 ...
|
||||
- `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 ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/service/pdfjson/NoOpJobOwnershipService.java:17` - 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` - 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` - 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` - 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` - 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 ...
|
||||
- `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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:24` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyJobStore.java:41` - @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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyKeyValueCache.java:18` - @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 ...
|
||||
- `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. 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/engine/EngineCapabilityClient.java:39` - @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` - 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` - 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" / ...
|
||||
- `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 ...
|
||||
- `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 & ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java:31` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/model/api/Email.java:11` - 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` - 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/security/TeamSecurityExpressions.java:27` - @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 ...
|
||||
- `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. 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 ...
|
||||
- `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. 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` - 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` - 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` - 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` - 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) -------- 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. 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). 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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditDashboardWebController.java:53` - 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` - 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. confirm command mapping. Quarkus exposes PING via the low-level command API: redisDataSource.execute("PING") returns ...
|
||||
- `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. the eager boot ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/ai/controller/AiProxyController.java:155` - 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. 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` - 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` - 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` - 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. 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. reimplement external ...
|
||||
- `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` - 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` - 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` - 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` - 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` - @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` - 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` - @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). any injection point that ...
|
||||
- `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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/ClusterNodeBootstrap.java:94` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:46` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/database/ScheduledTasks.java:23` - 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. the Spring cron was a SpEL expression ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:46` - 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` - @Async dropped (no Quarkus equivalent); now runs synchronously.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:125` - @Async dropped (no Quarkus equivalent); now runs synchronously.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:151` - @Async dropped (no Quarkus equivalent); now runs synchronously.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:211` - @Async dropped (no Quarkus equivalent); now runs synchronously.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/EmailService.java:257` - @Async dropped (no Quarkus equivalent); now runs synchronously.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/service/AiUserDataService.java:33` - 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/payg/job/StaleJobCloser.java:49` - 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` - 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` - @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` - @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` - 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. 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` - 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` - 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` - 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 ...
|
||||
- `app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java:30` - 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. 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 ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPdfToCbzRequest.java:30` - 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` - 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` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/KeyPersistenceService.java:54` - 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. 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. 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 ...
|
||||
- `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. longer term, prefer {@code ...
|
||||
- `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. 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` - 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 ...
|
||||
- `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. {@code classpath:} resolution does not enumerate entries inside a packaged JAR. For uber-jar deployments, prefer serving these assets from ...
|
||||
- `app/common/src/main/java/stirling/software/common/util/SpringContextHolder.java:66` - 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` - 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` - 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. if true per-HTTP-request semantics are needed, wrap the value in a ...
|
||||
- `app/core/src/main/java/stirling/software/SPDF/config/InitialSetup.java:21` - 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` - 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) ...
|
||||
- `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. 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}). 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 ...
|
||||
- `app/core/src/main/resources/application.properties:113` - ---- Jackson (was spring.jackson.*) ---------------------------------------------------------- spring.jackson.deserialization.fail-on-null-for-primitives=false 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 ------------------------------------------------------------------- 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. 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 createBaseAuditData must accept InvocationContext (ctx) once AuditService is migrated off ProceedingJoinPoint.
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:124` - addFileData must accept InvocationContext (ctx).
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:148` - addMethodArguments must accept InvocationContext (ctx).
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java:199` - 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` - this single {@code @AroundInvoke} replaces the five Spring {@code @Around} advice (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 ...
|
||||
- `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 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. Spring @DependsOn ordering relative to the Valkey connection config has no direct Quarkus equivalent. Ensure the Valkey/Redis bean either @Inject's this ...
|
||||
- `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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:69` - 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. 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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyConnectionConfiguration.java:252` - 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. propagate password/TLS auth from the parsed endpoint onto the RedisURI once cluster.valkey ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/cluster/valkey/ValkeyRateLimitStore.java:39` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:22` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java:90` - 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}. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java:68` - 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 ...
|
||||
- `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. 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/mcp/security/McpSecurityConfig.java:95` - 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` - 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` - 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 callers must adapt to the PanacheQuery return type (see class doc). ---------------------------------------------------------------------
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/DatabaseConfig.java:129` - 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 ...
|
||||
- `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. the SMTP connection settings previously configured programmatically from {@link ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/PasswordEncoderConfig.java:23` - 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. {@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 ...
|
||||
- `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). 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` - 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` - 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` - 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` - @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 / ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/database/H2SQLCondition.java:10` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/DatabaseService.java:317` - 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` - 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` - 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` - 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 ...
|
||||
- `app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java:632` - 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` - 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` - 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. 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} ...
|
||||
- `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. 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 ...
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:131` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:418` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:426` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:467` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
|
||||
- `app/saas/src/main/java/stirling/software/saas/controller/SaasTeamController.java:475` - replace Spring TransactionAspectSupport rollback-only with jakarta TransactionSynchronizationRegistry.setRollbackOnly() (injected).
|
||||
- `app/saas/src/main/java/stirling/software/saas/payg/lineage/LineagePruneScheduler.java:47` - 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` - 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` - 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` - 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` - 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` - 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). 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.
|
||||
|
||||
@@ -266,6 +266,20 @@ tasks:
|
||||
cmds:
|
||||
- task: frontend:lint
|
||||
- task: engine:lint
|
||||
- task: comment-lint
|
||||
|
||||
comment-lint:
|
||||
desc: "Check comment quality on the lines this branch adds"
|
||||
aliases: [comments]
|
||||
cmds:
|
||||
- task: pre-commit:comment-lint
|
||||
vars: { CLI_ARGS: '{{.CLI_ARGS}}' }
|
||||
|
||||
comment-lint:branch:
|
||||
desc: "Check comment quality on everything this branch adds over its base"
|
||||
cmds:
|
||||
- task: pre-commit:comment-lint:branch
|
||||
vars: { BASE: '{{.BASE}}' }
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix all components"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"allowedLicenses": [
|
||||
{
|
||||
"moduleName": "org.jboss:jboss-transaction-spi",
|
||||
"moduleLicense": "Public Domain"
|
||||
},
|
||||
{
|
||||
"moduleName": ".*",
|
||||
"moduleLicense": "BSD License"
|
||||
|
||||
+32
-7
@@ -1,15 +1,32 @@
|
||||
// Configure bootRun to disable it or point to a main class
|
||||
bootRun {
|
||||
enabled = false
|
||||
}
|
||||
// REMOVED: bootRun{enabled=false} - Spring Boot plugin task. :common is a Quarkus library module.
|
||||
// Spotless config is applied to every subproject from gradle/spotless.gradle.
|
||||
dependencies {
|
||||
// Security-hardening utilities (zip-slip, SSRF, filename sanitization, command injection).
|
||||
// Declared as api here so core + proprietary (which depend on common) get it transitively,
|
||||
// keeping it off modules that don't need it (e.g. saas).
|
||||
api 'io.github.pixee:java-security-toolkit:1.2.3'
|
||||
api "com.google.guava:guava:${guavaVersion}"
|
||||
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
|
||||
// spring-boot-starter-webmvc -> Quarkus REST stack (api-scoped so downstream modules inherit it).
|
||||
api 'io.quarkus:quarkus-rest'
|
||||
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.
|
||||
api 'io.quarkus:quarkus-undertow'
|
||||
// Bean Validation (was transitively in spring-boot-starter-webmvc).
|
||||
api 'io.quarkus:quarkus-hibernate-validator'
|
||||
// @Scheduled support (was spring-context scheduling). quarkus-scheduler manages its own
|
||||
// executor; the former SchedulingConfig TaskScheduler bean is no longer needed.
|
||||
api 'io.quarkus:quarkus-scheduler'
|
||||
// BCrypt implementation backing the Spring Security PasswordEncoder compatibility shim
|
||||
// (replaces spring-security-crypto's BCryptPasswordEncoder). Standalone, no framework.
|
||||
api 'at.favre.lib:bcrypt:0.10.2'
|
||||
// Swagger/OpenAPI annotations (io.swagger.v3.oas.annotations.*) used by common's API marker
|
||||
// interfaces; was transitive via springdoc. Quarkus' SmallRye OpenAPI also understands these.
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.53'
|
||||
// REMOVED: spring-boot-starter-aspectj. Quarkus has no AspectJ weaving; quarkus-arc provides
|
||||
// CDI interceptors (@AroundInvoke / interceptor bindings) instead.
|
||||
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'
|
||||
@@ -23,7 +40,8 @@ dependencies {
|
||||
api 'com.github.junrar:junrar:8.0.0' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:3.1.1'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3"
|
||||
// springdoc-openapi-starter-webmvc-ui -> SmallRye OpenAPI (schema at /q/openapi, UI at /q/swagger-ui)
|
||||
api 'io.quarkus:quarkus-smallrye-openapi'
|
||||
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
||||
api 'org.simplejavamail:simple-java-mail:9.3.2'
|
||||
// MSG file support; exclude commons-math3 (only HSSF/formula needs it, MSG parsing doesn't)
|
||||
@@ -83,6 +101,13 @@ dependencies {
|
||||
runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}"
|
||||
}
|
||||
|
||||
// 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.
|
||||
api 'tools.jackson.core:jackson-databind:3.0.0'
|
||||
api 'tools.jackson.core:jackson-core:3.0.0'
|
||||
|
||||
// Bucket4j (local in-process token bucket for RateLimitStore default impl)
|
||||
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Instance;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -16,7 +17,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.PdfaLevelAServiceInterface;
|
||||
|
||||
@Service
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class EndpointConfiguration {
|
||||
|
||||
@@ -48,21 +49,23 @@ public class EndpointConfiguration {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@Getter private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
private Set<String> disabledGroups = new HashSet<>();
|
||||
private Set<String> disabledGroups = ConcurrentHashMap.newKeySet();
|
||||
private Map<String, DisableReason> endpointDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, DisableReason> groupDisableReasons = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
||||
private final boolean runningProOrHigher;
|
||||
private final boolean pdfUaAvailable;
|
||||
|
||||
@Inject
|
||||
public EndpointConfiguration(
|
||||
ApplicationProperties applicationProperties,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||
@Autowired(required = false) PdfaLevelAServiceInterface pdfaLevelAService) {
|
||||
@Named("runningProOrHigher") boolean runningProOrHigher,
|
||||
Instance<PdfaLevelAServiceInterface> pdfaLevelAService) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
// The PDF/UA tagger ships in the proprietary module, and so do its endpoints.
|
||||
this.pdfUaAvailable = pdfaLevelAService != null;
|
||||
// MIGRATION: @Autowired(required = false) -> CDI Instance<>, resolved via isResolvable().
|
||||
this.pdfUaAvailable = pdfaLevelAService != null && pdfaLevelAService.isResolvable();
|
||||
init();
|
||||
processEnvironmentConfigs();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -47,7 +48,7 @@ import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
||||
* <li>Rotated tables (90°/270° pages) may produce incorrect bounds.
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class TabulaTableParser implements TableParser {
|
||||
|
||||
@@ -237,7 +238,7 @@ public class TabulaTableParser implements TableParser {
|
||||
score -= 0.3f;
|
||||
}
|
||||
|
||||
return Math.max(0f, Math.min(1f, score));
|
||||
return Math.clamp(score, 0f, 1f);
|
||||
}
|
||||
|
||||
private Bounds tableBounds(Table table) {
|
||||
|
||||
+29
-14
@@ -2,21 +2,27 @@ package stirling.software.common.annotations;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
|
||||
import jakarta.enterprise.util.Nonbinding;
|
||||
import jakarta.interceptor.InterceptorBinding;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
|
||||
/**
|
||||
* Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework.
|
||||
*
|
||||
* <p>MIGRATION (Spring -> Quarkus): this was a Spring composed meta-annotation that stamped
|
||||
* {@code @RequestMapping(method=POST)} onto the target via {@code @AliasFor}. JAX-RS does not
|
||||
* honour {@code @Path}/{@code @POST}/{@code @Consumes} through meta-annotations, so this annotation
|
||||
* no longer provides routing. It is now a CDI {@link InterceptorBinding} handled by {@code
|
||||
* AutoJobInterceptor}. <b>Controllers using {@code @AutoJobPostMapping} must additionally declare
|
||||
* their own JAX-RS {@code @POST} + {@code @Path(value)} + {@code @Consumes(consumes)}.</b> The
|
||||
* {@link #value()}/{@link #consumes()} members are retained so a scanner/controller can read the
|
||||
* intended routing.
|
||||
*
|
||||
* <p>Behaviour notes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The endpoint is registered with {@code POST} and, by default, consumes {@code
|
||||
* multipart/form-data} unless you override {@link #consumes()}.
|
||||
* <li>When the client supplies {@code ?async=true} the call is handed to {@link
|
||||
* stirling.software.common.service.JobExecutorService JobExecutorService} where it may be
|
||||
* queued, retried, tracked and subject to time‑outs. For synchronous (default) invocations
|
||||
@@ -26,22 +32,26 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
* GET /api/v1/general/job/{id}</code>.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Unless stated otherwise an attribute only affects <em>async</em> execution.
|
||||
* <p>Unless stated otherwise an attribute only affects <em>async</em> execution. All members are
|
||||
* {@code @Nonbinding} so the single {@code AutoJobInterceptor} matches every annotated method; the
|
||||
* interceptor reads the actual values reflectively from the target method.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
@InterceptorBinding
|
||||
@RequestBody(required = true)
|
||||
public @interface AutoJobPostMapping {
|
||||
|
||||
/** Alias for {@link RequestMapping#value} – the path mapping of the endpoint. */
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "value")
|
||||
/**
|
||||
* The path mapping of the endpoint (controllers must mirror this on a JAX-RS {@code @Path}).
|
||||
*/
|
||||
@Nonbinding
|
||||
String[] value() default {};
|
||||
|
||||
/** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */
|
||||
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
||||
String[] consumes() default {MediaType.MULTIPART_FORM_DATA_VALUE};
|
||||
@Nonbinding
|
||||
String[] consumes() default {MediaType.MULTIPART_FORM_DATA};
|
||||
|
||||
/**
|
||||
* Maximum execution time in milliseconds before the job is aborted. A negative value means "use
|
||||
@@ -49,6 +59,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
long timeout() default -1;
|
||||
|
||||
/**
|
||||
@@ -57,6 +68,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
int retryCount() default 1;
|
||||
|
||||
/**
|
||||
@@ -64,6 +76,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
boolean trackProgress() default true;
|
||||
|
||||
/**
|
||||
@@ -72,6 +85,7 @@ public @interface AutoJobPostMapping {
|
||||
*
|
||||
* <p>Only honoured when {@code async=true}.
|
||||
*/
|
||||
@Nonbinding
|
||||
boolean queueable() default false;
|
||||
|
||||
/**
|
||||
@@ -82,5 +96,6 @@ public @interface AutoJobPostMapping {
|
||||
* AutoJobPostMappingWeightTest} fails the build if any endpoint leaves it unset. Runtime
|
||||
* readers clamp the value into {@code [1, 100]}.
|
||||
*/
|
||||
@Nonbinding
|
||||
int resourceWeight() default Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
+3
-5
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/account")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/account").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Account Security",
|
||||
description =
|
||||
|
||||
@@ -5,19 +5,20 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Admin Settings API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/admin/settings"), and OpenAPI @Tag.
|
||||
*
|
||||
* <p>MIGRATION (Spring -> JAX-RS): JAX-RS/RESTEasy does NOT process {@code @Path} via custom
|
||||
* meta-annotations (Spring honoured composed {@code @RestController}/{@code @RequestMapping}
|
||||
* through {@code @AliasFor}; JAX-RS has no equivalent). This annotation therefore now carries only
|
||||
* the OpenAPI {@code @Tag}. Each controller annotated with {@code @AdminApi} MUST additionally
|
||||
* declare its own {@code @jakarta.ws.rs.Path("/api/v1/admin/settings")} (the path the removed
|
||||
* {@code @RequestMapping} used to supply).
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/settings")
|
||||
@Tag(
|
||||
name = "Admin Settings",
|
||||
description =
|
||||
|
||||
+3
-5
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/server-certificate")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/admin/server-certificate").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Admin - Server Certificate",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/analysis")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/analysis").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Analysis",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/config")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/config").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Config",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/convert").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Convert",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/database")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/database").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Database",
|
||||
description =
|
||||
|
||||
+3
-5
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/database")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/admin/database").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Database Management",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/filter")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/filter").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Filter",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/general").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "General",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/info")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/info").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Info",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/invite")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/invite").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Invite",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/misc").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Misc",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pipeline")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/pipeline").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Pipeline",
|
||||
description =
|
||||
|
||||
+3
-5
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -17,8 +14,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proprietary/ui-data")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/proprietary/ui-data").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Proprietary UI Data",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/security").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Security",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/settings")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/settings").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Settings",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/team")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/team").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "Team",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ui-data")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/ui-data").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "UI Data",
|
||||
description =
|
||||
|
||||
@@ -5,9 +5,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/user")
|
||||
// MIGRATION (Spring->JAX-RS): controllers using this annotation must declare
|
||||
// @jakarta.ws.rs.Path("/api/v1/user").
|
||||
// JAX-RS does not honour @Path via meta-annotations, so the path is not inherited from here.
|
||||
@Tag(
|
||||
name = "User",
|
||||
description =
|
||||
|
||||
@@ -9,46 +9,101 @@ import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.interceptor.AroundInvoke;
|
||||
import jakarta.interceptor.Interceptor;
|
||||
import jakarta.interceptor.InvocationContext;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
/**
|
||||
* MIGRATION (Spring AOP -> CDI interceptor): was an {@code @Aspect} with {@code @Around} advice on
|
||||
* {@code @AutoJobPostMapping}. Reworked into a CDI {@link Interceptor} bound by the
|
||||
* {@code @AutoJobPostMapping} {@code @InterceptorBinding}; {@code @Around}/{@code
|
||||
* ProceedingJoinPoint} became {@code @AroundInvoke}/{@link InvocationContext}.
|
||||
* {@code @Priority(20)} now meaningfully orders this interceptor (runs after lower-priority audit
|
||||
* interceptors populate MDC).
|
||||
*/
|
||||
@Interceptor
|
||||
@AutoJobPostMapping
|
||||
@Priority(20)
|
||||
@Slf4j
|
||||
@Order(20) // Lower precedence - executes AFTER audit aspects populate MDC
|
||||
public class AutoJobAspect {
|
||||
|
||||
private static final Duration RETRY_BASE_DELAY = Duration.ofMillis(100);
|
||||
|
||||
private final JobExecutorService jobExecutorService;
|
||||
private final HttpServletRequest request;
|
||||
// Reactive-safe access to the current request. The undertow HttpServletRequest proxy throws
|
||||
// UT000048 ("No request is currently active") on RESTEasy Reactive worker threads, so query
|
||||
// params / method / path / attributes are read from the Vert.x request instead, degrading to
|
||||
// null/empty when no request is active.
|
||||
private final CurrentVertxRequest currentVertxRequest;
|
||||
private final FileStorage fileStorage;
|
||||
|
||||
@Around("@annotation(autoJobPostMapping)")
|
||||
public Object wrapWithJobExecution(
|
||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) throws Exception {
|
||||
// This aspect will run before any audit aspects due to @Order(0)
|
||||
@Inject
|
||||
public AutoJobAspect(
|
||||
JobExecutorService jobExecutorService,
|
||||
CurrentVertxRequest currentVertxRequest,
|
||||
FileStorage fileStorage) {
|
||||
this.jobExecutorService = jobExecutorService;
|
||||
this.currentVertxRequest = currentVertxRequest;
|
||||
this.fileStorage = fileStorage;
|
||||
}
|
||||
|
||||
private io.vertx.core.http.HttpServerRequest vertxRequest() {
|
||||
try {
|
||||
var current = currentVertxRequest.getCurrent();
|
||||
return current != null ? current.request() : null;
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String requestParam(String name) {
|
||||
io.vertx.core.http.HttpServerRequest req = vertxRequest();
|
||||
return req != null ? req.getParam(name) : null;
|
||||
}
|
||||
|
||||
private String requestMethod() {
|
||||
io.vertx.core.http.HttpServerRequest req = vertxRequest();
|
||||
return req != null ? req.method().name() : "";
|
||||
}
|
||||
|
||||
private String requestUri() {
|
||||
io.vertx.core.http.HttpServerRequest req = vertxRequest();
|
||||
return req != null ? req.path() : "";
|
||||
}
|
||||
|
||||
private Object requestAttribute(String name) {
|
||||
try {
|
||||
var current = currentVertxRequest.getCurrent();
|
||||
return current != null ? current.get(name) : null;
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@AroundInvoke
|
||||
public Object wrapWithJobExecution(InvocationContext ctx) throws Exception {
|
||||
AutoJobPostMapping autoJobPostMapping =
|
||||
ctx.getMethod().getAnnotation(AutoJobPostMapping.class);
|
||||
// Extract parameters from the request and annotation
|
||||
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||
boolean async = Boolean.parseBoolean(requestParam("async"));
|
||||
log.debug(
|
||||
"AutoJobAspect: Processing {} {} with async={}",
|
||||
request.getMethod(),
|
||||
request.getRequestURI(),
|
||||
requestMethod(),
|
||||
requestUri(),
|
||||
async);
|
||||
long timeout = autoJobPostMapping.timeout();
|
||||
int retryCount = autoJobPostMapping.retryCount();
|
||||
@@ -63,7 +118,8 @@ public class AutoJobAspect {
|
||||
trackProgress);
|
||||
|
||||
// Process arguments in-place to avoid type mismatch issues
|
||||
Object[] args = processArgsInPlace(joinPoint.getArgs(), async);
|
||||
Object[] args = processArgsInPlace(ctx.getParameters(), async);
|
||||
ctx.setParameters(args);
|
||||
|
||||
// Extract queueable and resourceWeight parameters and validate
|
||||
boolean queueable = autoJobPostMapping.queueable();
|
||||
@@ -82,7 +138,7 @@ public class AutoJobAspect {
|
||||
// The trackProgress flag controls whether detailed progress is
|
||||
// stored
|
||||
// for REST API queries, not WebSocket notifications
|
||||
return joinPoint.proceed(args);
|
||||
return ctx.proceed();
|
||||
} catch (Throwable ex) {
|
||||
log.error(
|
||||
"AutoJobAspect caught exception during job execution: {}",
|
||||
@@ -103,7 +159,7 @@ public class AutoJobAspect {
|
||||
} else {
|
||||
// Use retry logic
|
||||
return executeWithRetries(
|
||||
joinPoint,
|
||||
ctx,
|
||||
args,
|
||||
async,
|
||||
timeout,
|
||||
@@ -115,7 +171,7 @@ public class AutoJobAspect {
|
||||
}
|
||||
|
||||
private Object executeWithRetries(
|
||||
ProceedingJoinPoint joinPoint,
|
||||
InvocationContext ctx,
|
||||
Object[] args,
|
||||
boolean async,
|
||||
long timeout,
|
||||
@@ -160,7 +216,7 @@ public class AutoJobAspect {
|
||||
}
|
||||
|
||||
// Attempt to execute the operation
|
||||
return joinPoint.proceed(args);
|
||||
return ctx.proceed();
|
||||
|
||||
} catch (Throwable ex) {
|
||||
lastException = ex;
|
||||
@@ -300,13 +356,17 @@ public class AutoJobAspect {
|
||||
@SuppressWarnings("unchecked")
|
||||
private void recordPendingInputFile(String fileId) {
|
||||
try {
|
||||
Object existing = request.getAttribute(JobExecutorService.PENDING_INPUT_FILE_IDS_ATTR);
|
||||
var current = currentVertxRequest.getCurrent();
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("no active request");
|
||||
}
|
||||
Object existing = current.get(JobExecutorService.PENDING_INPUT_FILE_IDS_ATTR);
|
||||
List<String> ids;
|
||||
if (existing instanceof List<?> list) {
|
||||
ids = (List<String>) list;
|
||||
} else {
|
||||
ids = new ArrayList<>();
|
||||
request.setAttribute(JobExecutorService.PENDING_INPUT_FILE_IDS_ATTR, ids);
|
||||
current.put(JobExecutorService.PENDING_INPUT_FILE_IDS_ATTR, ids);
|
||||
}
|
||||
ids.add(fileId);
|
||||
} catch (RuntimeException ex) {
|
||||
@@ -318,7 +378,7 @@ public class AutoJobAspect {
|
||||
|
||||
private String getJobIdFromContext() {
|
||||
try {
|
||||
return (String) request.getAttribute("jobId");
|
||||
return (String) requestAttribute("jobId");
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not retrieve job ID from context: {}", e.getMessage());
|
||||
return null;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package stirling.software.common.cluster;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -19,7 +18,7 @@ import stirling.software.common.model.ApplicationProperties.Cluster;
|
||||
* single-instance install needs no new config.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
public class ClusterConfig {
|
||||
|
||||
@@ -47,7 +46,7 @@ public class ClusterConfig {
|
||||
+ " JVM is coordinated. Cross-node lookups and the file proxy will fail."
|
||||
+ " Use backplane=valkey for real multi-node deployments.");
|
||||
} else {
|
||||
// Fail fast on typos like "valky" so Spring doesn't later report a cryptic
|
||||
// Fail fast on typos like "valky" so CDI doesn't later report a cryptic
|
||||
// "no ClusterBackplane bean" - the operator-facing error names the bad value.
|
||||
throw new IllegalStateException(
|
||||
"cluster.enabled=true with unknown backplane '"
|
||||
|
||||
+23
-20
@@ -1,9 +1,9 @@
|
||||
package stirling.software.common.cluster.inprocess;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import io.quarkus.arc.DefaultBean;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -20,45 +20,48 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
* cluster mode is off or {@code cluster.backplane=inprocess}.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnExpression(
|
||||
"!${cluster.enabled:false} ||"
|
||||
+ " '${cluster.backplane:inprocess}'.equalsIgnoreCase('inprocess')")
|
||||
@ApplicationScoped
|
||||
public class InProcessClusterConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Produces
|
||||
@DefaultBean
|
||||
@ApplicationScoped
|
||||
public ClusterBackplane clusterBackplane(ApplicationProperties applicationProperties) {
|
||||
log.info("Cluster backplane: in-process (single node)");
|
||||
return new InProcessClusterBackplane(applicationProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Produces
|
||||
@DefaultBean
|
||||
@ApplicationScoped
|
||||
public JobStore jobStore() {
|
||||
return new InProcessJobStore();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Produces
|
||||
@DefaultBean
|
||||
@ApplicationScoped
|
||||
public RateLimitStore rateLimitStore() {
|
||||
return new InProcessRateLimitStore();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Produces
|
||||
@DefaultBean
|
||||
@ApplicationScoped
|
||||
public DistributedLock distributedLock() {
|
||||
return new InProcessDistributedLock();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Produces
|
||||
@DefaultBean
|
||||
@ApplicationScoped
|
||||
public KeyValueCache keyValueCache() {
|
||||
return new InProcessKeyValueCache();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Produces
|
||||
@DefaultBean
|
||||
@ApplicationScoped
|
||||
public InstanceRegistry instanceRegistry() {
|
||||
return new InProcessInstanceRegistry();
|
||||
}
|
||||
|
||||
+13
-14
@@ -1,10 +1,11 @@
|
||||
package stirling.software.common.cluster.inprocess;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import io.quarkus.arc.DefaultBean;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
|
||||
import stirling.software.common.cluster.FileStore;
|
||||
|
||||
@@ -13,17 +14,15 @@ 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}.
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(
|
||||
prefix = "cluster",
|
||||
name = "artifactStore",
|
||||
havingValue = "local",
|
||||
matchIfMissing = true)
|
||||
@ApplicationScoped
|
||||
public class LocalDiskFileStoreConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public FileStore fileStore(@Value("${stirling.tempDir:/tmp/stirling-files}") String tempDir) {
|
||||
@Produces
|
||||
@DefaultBean
|
||||
@ApplicationScoped
|
||||
public FileStore fileStore(
|
||||
@ConfigProperty(name = "stirling.tempDir", defaultValue = "/tmp/stirling-files")
|
||||
String tempDir) {
|
||||
return new LocalDiskFileStore(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-14
@@ -3,37 +3,29 @@ package stirling.software.common.config;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
/**
|
||||
* Configuration for the temporary file management system. Sets up the necessary beans and
|
||||
* configures system properties.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ApplicationScoped
|
||||
@RequiredArgsConstructor
|
||||
public class TempFileConfiguration {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
/**
|
||||
* Create the TempFileRegistry bean.
|
||||
*
|
||||
* @return A new TempFileRegistry instance
|
||||
*/
|
||||
@Bean
|
||||
public TempFileRegistry tempFileRegistry() {
|
||||
return new TempFileRegistry();
|
||||
}
|
||||
// MIGRATION: the @Produces TempFileRegistry producer was removed. TempFileRegistry is already
|
||||
// an
|
||||
// @ApplicationScoped CDI bean with a no-arg constructor, so the producer was a redundant second
|
||||
// @Default bean of the same type and made every injection point ambiguous.
|
||||
|
||||
@PostConstruct
|
||||
public void initTempFileConfig() {
|
||||
|
||||
@@ -5,8 +5,8 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -14,12 +14,12 @@ import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
/**
|
||||
* Handles cleanup of temporary files on application shutdown. Implements Spring's DisposableBean
|
||||
* interface to ensure cleanup happens during normal application shutdown.
|
||||
* Handles cleanup of temporary files on application shutdown. Uses a CDI {@code @PreDestroy} method
|
||||
* (migrated from Spring's {@code DisposableBean}) to ensure cleanup happens during normal shutdown.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TempFileShutdownHook implements DisposableBean {
|
||||
@ApplicationScoped
|
||||
public class TempFileShutdownHook {
|
||||
|
||||
private final TempFileRegistry registry;
|
||||
|
||||
@@ -31,8 +31,8 @@ public class TempFileShutdownHook implements DisposableBean {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::cleanupTempFiles));
|
||||
}
|
||||
|
||||
/** Spring's DisposableBean interface method. Called during normal application shutdown. */
|
||||
@Override
|
||||
/** CDI pre-destroy callback (was DisposableBean#destroy). Called during normal shutdown. */
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
log.info("Application shutting down, cleaning up temporary files");
|
||||
cleanupTempFiles();
|
||||
|
||||
+80
-45
@@ -1,25 +1,24 @@
|
||||
package stirling.software.common.config.swagger;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springdoc.core.customizers.GlobalOperationCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
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.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.Operation;
|
||||
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.ToolIOCase;
|
||||
import stirling.software.common.model.tool.ToolIOWhen;
|
||||
import stirling.software.common.service.ToolIOParameterDefaults;
|
||||
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
|
||||
@@ -27,66 +26,99 @@ import stirling.software.common.service.ToolIOParameterDefaults;
|
||||
*
|
||||
* <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}.
|
||||
*/
|
||||
@Component
|
||||
public class ToolIOOperationCustomizer
|
||||
implements GlobalOperationCustomizer, GlobalOpenApiCustomizer {
|
||||
@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.
|
||||
@Override
|
||||
public void customise(OpenAPI openApi) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
|
||||
ToolIO declaration = handlerMethod.getMethodAnnotation(ToolIO.class);
|
||||
if (declaration == null) {
|
||||
return operation;
|
||||
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;
|
||||
}
|
||||
operation.addExtension(EXTENSION_NAME, toExtension(declaration, handlerMethod.getMethod()));
|
||||
operation.setDescription(appendSummaryLine(operation.getDescription(), declaration));
|
||||
return operation;
|
||||
}
|
||||
|
||||
private static Map<String, Object> toExtension(ToolIO declaration, Method handler) {
|
||||
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(declaration.accepts()));
|
||||
extension.put("produces", declaration.produces().name());
|
||||
extension.put("arity", declaration.arity().name());
|
||||
if (declaration.cases().length > 0) {
|
||||
extension.put("cases", cases(declaration, handler));
|
||||
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(ToolIO declaration, Method handler) {
|
||||
return Arrays.stream(declaration.cases()).map(rule -> toCase(rule, handler)).toList();
|
||||
private static List<Map<String, Object>> cases(ToolIOSpec spec) {
|
||||
return spec.cases().stream().map(ToolIOOperationCustomizer::toCase).toList();
|
||||
}
|
||||
|
||||
private static Map<String, Object> toCase(ToolIOCase rule, Method handler) {
|
||||
private static Map<String, Object> toCase(ToolIOSpec.Case rule) {
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("when", Arrays.stream(rule.when()).map(c -> toCondition(c, handler)).toList());
|
||||
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(ToolIOWhen condition, Method handler) {
|
||||
private static Map<String, Object> toCondition(ToolIOSpec.When condition) {
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("param", condition.param());
|
||||
entry.put("matches", List.of(condition.matches()));
|
||||
entry.put("matches", List.copyOf(condition.matches()));
|
||||
// The default the endpoint uses when this parameter is absent, so a step that never sends
|
||||
// it still resolves. Omitted when the parameter is required with none.
|
||||
ToolIOParameterDefaults.resolve(handler, condition.param())
|
||||
.ifPresent(value -> entry.put("default", value));
|
||||
// it still resolves. Omitted when the parameter is required with none - the registry
|
||||
// resolves it off the handler method when it builds the spec.
|
||||
if (condition.paramDefault() != null) {
|
||||
entry.put("default", condition.paramDefault());
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -94,16 +126,19 @@ public class ToolIOOperationCustomizer
|
||||
return Arrays.stream(values).map(Enum::name).toList();
|
||||
}
|
||||
|
||||
private static String appendSummaryLine(String description, ToolIO declaration) {
|
||||
private static String appendSummaryLine(String description, ToolIOSpec spec) {
|
||||
String summary =
|
||||
"Input:"
|
||||
+ String.join("/", names(declaration.accepts()))
|
||||
+ String.join("/", names(spec.accepts().toArray(ToolFormat[]::new)))
|
||||
+ " Output:"
|
||||
+ declaration.produces().name()
|
||||
+ spec.produces().name()
|
||||
+ " Type:"
|
||||
+ declaration.arity().name();
|
||||
return description == null || description.isBlank()
|
||||
? summary
|
||||
: description.trim() + " " + summary;
|
||||
+ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,40 +9,65 @@ import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.eclipse.microprofile.config.Config;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.context.Dependent;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Lazy
|
||||
/**
|
||||
* Central CDI producer hub (migrated from a Spring {@code @Configuration} class).
|
||||
*
|
||||
* <p>MIGRATION NOTES (Spring -> Quarkus CDI):
|
||||
*
|
||||
* <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 @Scope("request")} on {@code boolean} producers -> {@code @Dependent}. CDI normal
|
||||
* 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.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@ApplicationScoped
|
||||
public class AppConfig {
|
||||
|
||||
private final Environment env;
|
||||
private final Config config;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Getter
|
||||
@Value("${server.servlet.context-path:/}")
|
||||
private String contextPath;
|
||||
@ConfigProperty(name = "server.servlet.context-path", defaultValue = "/")
|
||||
String contextPath;
|
||||
|
||||
@Getter
|
||||
@Value("${server.port:8080}")
|
||||
private String serverPort;
|
||||
@ConfigProperty(name = "quarkus.http.port", defaultValue = "8080")
|
||||
String serverPort;
|
||||
|
||||
@ConfigProperty(name = "v2")
|
||||
boolean v2Enabled;
|
||||
|
||||
@Inject
|
||||
public AppConfig(Config config, ApplicationProperties applicationProperties) {
|
||||
this.config = config;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backend URL from system configuration. Falls back to http://localhost if not
|
||||
@@ -55,76 +80,110 @@ public class AppConfig {
|
||||
return (backendUrl != null && !backendUrl.isBlank()) ? backendUrl : "http://localhost";
|
||||
}
|
||||
|
||||
@Value("${v2}")
|
||||
public boolean v2Enabled;
|
||||
|
||||
@Bean
|
||||
@Produces
|
||||
@Named("v2Enabled")
|
||||
public boolean v2Enabled() {
|
||||
return v2Enabled;
|
||||
}
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
// 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 code that uses the Jackson 3 API
|
||||
// directly.
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
public tools.jackson.databind.ObjectMapper jackson3ObjectMapper() {
|
||||
return tools.jackson.databind.json.JsonMapper.builder().build();
|
||||
}
|
||||
|
||||
@Produces
|
||||
@Named("contextPath")
|
||||
public String contextPathBean() {
|
||||
return contextPath;
|
||||
}
|
||||
|
||||
@Produces
|
||||
@Named("loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
return applicationProperties.getSecurity().isEnableLogin();
|
||||
}
|
||||
|
||||
@Bean(name = "appName")
|
||||
// MIGRATION: CDI has no producer for the nested ApplicationProperties.Security.SAML2 config
|
||||
// object, so beans that inject it directly (e.g. CustomSaml2AuthenticationSuccessHandler) were
|
||||
// unsatisfied. Expose it from the already-injected ApplicationProperties. May be null/disabled;
|
||||
// that is fine for injection.
|
||||
@Produces
|
||||
public ApplicationProperties.Security.SAML2 saml2Config() {
|
||||
return applicationProperties.getSecurity().getSaml2();
|
||||
}
|
||||
|
||||
@Produces
|
||||
@Named("appName")
|
||||
public String appName() {
|
||||
return "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "appVersion")
|
||||
@Produces
|
||||
@Named("appVersion")
|
||||
public String appVersion() {
|
||||
Resource resource = new ClassPathResource("version.properties");
|
||||
// MIGRATION: Spring ClassPathResource -> plain classloader resource lookup.
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
props.load(resource.getInputStream());
|
||||
return props.getProperty("version");
|
||||
try (var in = getClass().getClassLoader().getResourceAsStream("version.properties")) {
|
||||
if (in != null) {
|
||||
props.load(in);
|
||||
return props.getProperty("version");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
@Bean(name = "homeText")
|
||||
@Produces
|
||||
@Named("homeText")
|
||||
public String homeText() {
|
||||
return "null";
|
||||
}
|
||||
|
||||
@Bean(name = "languages")
|
||||
@Produces
|
||||
@Named("languages")
|
||||
public List<String> languages() {
|
||||
return applicationProperties.getUi().getLanguages();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String contextPath(@Value("${server.servlet.context-path}") String contextPath) {
|
||||
return contextPath;
|
||||
}
|
||||
|
||||
@Bean(name = "navBarText")
|
||||
@Produces
|
||||
@Named("navBarText")
|
||||
public String navBarText() {
|
||||
String navBar = applicationProperties.getUi().getAppNameNavbar();
|
||||
return (navBar != null) ? navBar : "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "enableAlphaFunctionality")
|
||||
@Produces
|
||||
@Named("enableAlphaFunctionality")
|
||||
public boolean enableAlphaFunctionality() {
|
||||
return applicationProperties.getSystem().isEnableAlphaFunctionality();
|
||||
}
|
||||
|
||||
@Bean(name = "rateLimit")
|
||||
@Produces
|
||||
@Named("rateLimit")
|
||||
public boolean rateLimit() {
|
||||
String rateLimit = System.getProperty("rateLimit");
|
||||
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
||||
return Boolean.parseBoolean(rateLimit);
|
||||
}
|
||||
|
||||
@Bean(name = "RunningInDocker")
|
||||
@Produces
|
||||
@Named("RunningInDocker")
|
||||
public boolean runningInDocker() {
|
||||
return Files.exists(Path.of("/.dockerenv"));
|
||||
}
|
||||
|
||||
@Bean(name = "configDirMounted")
|
||||
@Produces
|
||||
@Named("configDirMounted")
|
||||
public boolean isRunningInDockerWithConfig() {
|
||||
Path dockerEnv = Path.of("/.dockerenv");
|
||||
// default to true if not docker
|
||||
@@ -143,14 +202,23 @@ public class AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
@Bean(name = "activeSecurity")
|
||||
@Produces
|
||||
@Named("activeSecurity")
|
||||
public boolean missingActiveSecurity() {
|
||||
return ClassUtils.isPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
|
||||
this.getClass().getClassLoader());
|
||||
// MIGRATION: Spring ClassUtils.isPresent -> manual Class.forName presence check.
|
||||
try {
|
||||
Class.forName(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
|
||||
false,
|
||||
this.getClass().getClassLoader());
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Bean(name = "directoryFilter")
|
||||
@Produces
|
||||
@Named("directoryFilter")
|
||||
public Predicate<Path> processOnlyFiles() {
|
||||
return path -> {
|
||||
if (Files.isDirectory(path)) {
|
||||
@@ -161,113 +229,138 @@ public class AppConfig {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean(name = "termsAndConditions")
|
||||
@Produces
|
||||
@Named("termsAndConditions")
|
||||
public String termsAndConditions() {
|
||||
return applicationProperties.getLegal().getTermsAndConditions();
|
||||
}
|
||||
|
||||
@Bean(name = "privacyPolicy")
|
||||
@Produces
|
||||
@Named("privacyPolicy")
|
||||
public String privacyPolicy() {
|
||||
return applicationProperties.getLegal().getPrivacyPolicy();
|
||||
}
|
||||
|
||||
@Bean(name = "cookiePolicy")
|
||||
@Produces
|
||||
@Named("cookiePolicy")
|
||||
public String cookiePolicy() {
|
||||
return applicationProperties.getLegal().getCookiePolicy();
|
||||
}
|
||||
|
||||
@Bean(name = "impressum")
|
||||
@Produces
|
||||
@Named("impressum")
|
||||
public String impressum() {
|
||||
return applicationProperties.getLegal().getImpressum();
|
||||
}
|
||||
|
||||
@Bean(name = "accessibilityStatement")
|
||||
@Produces
|
||||
@Named("accessibilityStatement")
|
||||
public String accessibilityStatement() {
|
||||
return applicationProperties.getLegal().getAccessibilityStatement();
|
||||
}
|
||||
|
||||
@Bean(name = "analyticsPrompt")
|
||||
@Scope("request")
|
||||
@Produces
|
||||
@Dependent
|
||||
@Named("analyticsPrompt")
|
||||
public boolean analyticsPrompt() {
|
||||
return applicationProperties.getSystem().getEnableAnalytics() == null;
|
||||
}
|
||||
|
||||
@Bean(name = "analyticsEnabled")
|
||||
@Scope("request")
|
||||
@Produces
|
||||
@Dependent
|
||||
@Named("analyticsEnabled")
|
||||
public boolean analyticsEnabled() {
|
||||
if (applicationProperties.getPremium().isEnabled()) return true;
|
||||
return applicationProperties.getSystem().isAnalyticsEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "StirlingPDFLabel")
|
||||
@Produces
|
||||
@Named("StirlingPDFLabel")
|
||||
public String stirlingPDFLabel() {
|
||||
return "Stirling-PDF" + " v" + appVersion();
|
||||
}
|
||||
|
||||
@Bean(name = "UUID")
|
||||
@Produces
|
||||
@Named("UUID")
|
||||
public String uuid() {
|
||||
return applicationProperties.getAutomaticallyGenerated().getUUID();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Produces
|
||||
public ApplicationProperties.Security security() {
|
||||
return applicationProperties.getSecurity();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Produces
|
||||
public ApplicationProperties.Security.OAUTH2 oAuth2() {
|
||||
return applicationProperties.getSecurity().getOauth2();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Produces
|
||||
public ApplicationProperties.Premium premium() {
|
||||
return applicationProperties.getPremium();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Produces
|
||||
public ApplicationProperties.System system() {
|
||||
return applicationProperties.getSystem();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Produces
|
||||
public ApplicationProperties.Datasource datasource() {
|
||||
return applicationProperties.getSystem().getDatasource();
|
||||
}
|
||||
|
||||
@Bean(name = "runningProOrHigher")
|
||||
@Profile("default")
|
||||
// @IfBuildProfile("core"): these NORMAL/default license @Named beans apply only to the core
|
||||
// flavor. In proprietary EEAppConfig provides them (security profile) and in saas
|
||||
// SaasLicenseOverride does (saas profile); registering this producer alongside those trips
|
||||
// Qute's named-bean validation ("Duplicate key runningEE"), which does not honour @DefaultBean
|
||||
// suppression - so gate to core outright. (In core, EEAppConfig/SaasLicenseOverride are not
|
||||
// even on the classpath.)
|
||||
@Produces
|
||||
@IfBuildProfile("core")
|
||||
@Named("runningProOrHigher")
|
||||
public boolean runningProOrHigher() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "runningEE")
|
||||
@Profile("default")
|
||||
@Produces
|
||||
@IfBuildProfile("core")
|
||||
@Named("runningEE")
|
||||
public boolean runningEnterprise() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "license")
|
||||
@Profile("default")
|
||||
@Produces
|
||||
@IfBuildProfile("core")
|
||||
@Named("license")
|
||||
public String licenseType() {
|
||||
return "NORMAL";
|
||||
}
|
||||
|
||||
@Bean(name = "scarfEnabled")
|
||||
@Produces
|
||||
@Named("scarfEnabled")
|
||||
public boolean scarfEnabled() {
|
||||
return applicationProperties.getSystem().isScarfEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "posthogEnabled")
|
||||
@Produces
|
||||
@Named("posthogEnabled")
|
||||
public boolean posthogEnabled() {
|
||||
return applicationProperties.getSystem().isPosthogEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "machineType")
|
||||
@Produces
|
||||
@Named("machineType")
|
||||
public String determineMachineType() {
|
||||
try {
|
||||
boolean isDocker = runningInDocker();
|
||||
boolean isKubernetes = System.getenv("KUBERNETES_SERVICE_HOST") != null;
|
||||
boolean isBrowserOpen = "true".equalsIgnoreCase(env.getProperty("BROWSER_OPEN"));
|
||||
boolean isBrowserOpen =
|
||||
"true"
|
||||
.equalsIgnoreCase(
|
||||
config.getOptionalValue("BROWSER_OPEN", String.class)
|
||||
.orElse(null));
|
||||
|
||||
if (isKubernetes) {
|
||||
return "Kubernetes";
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.microprofile.config.Config;
|
||||
import org.eclipse.microprofile.config.ConfigProvider;
|
||||
|
||||
import io.quarkus.arc.ClientProxy;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.interceptor.Interceptor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
/**
|
||||
* Binds MicroProfile/Quarkus config (env vars, {@code settings.yml} via {@link
|
||||
* SettingsYamlConfigSource}, {@code application.properties}, system properties) onto the mutable
|
||||
* {@link ApplicationProperties} bean at startup - the Quarkus replacement for the Spring
|
||||
* {@code @ConfigurationProperties(prefix = "")} relaxed binding that was lost in the migration.
|
||||
*
|
||||
* <p>Rather than hand-listing each property, this walks the whole {@code ApplicationProperties}
|
||||
* object graph by reflection and, for every scalar / enum / scalar-list field, applies the value
|
||||
* from config when one is present (so unset fields keep their Java default). The dotted key for a
|
||||
* field mirrors its path in the tree ({@code security.oauth2.client.keycloak.clientId}, {@code
|
||||
* endpoints.toRemove}, ...); SmallRye then resolves it from any source - e.g. env var {@code
|
||||
* SECURITY_OAUTH2_CLIENT_KEYCLOAK_CLIENTID} or the same key in {@code settings.yml} - with the
|
||||
* usual precedence (sys props > env > settings.yml > application.properties).
|
||||
*
|
||||
* <p>This is the behaviour Spring had: every settings.yml / {@code SECURITY_*}/{@code STORAGE_*}
|
||||
* /{@code PREMIUM_*} value is honoured, fixing the whole {@code maxDPI=0} / {@code enableLogin}
|
||||
* /{@code endpoints.toRemove} / premium-license class of "ignored config" bugs at once.
|
||||
*
|
||||
* <p>Runs with {@code @Priority(APPLICATION)} so it completes before startup consumers read the
|
||||
* bean: {@code InitialSecuritySetup} (enableLogin / customGlobalAPIKey), {@code
|
||||
* EndpointConfiguration} (endpoints.toRemove), and {@code LicenseKeyChecker.onApplicationReady}
|
||||
* (premium.enabled / premium.key, which has the lower default observer priority 2500).
|
||||
*
|
||||
* <p>Values are never logged - only key names at DEBUG and a total at INFO - because the tree
|
||||
* carries secrets (premium key, client secrets, initial-login password, SMTP/Telegram tokens).
|
||||
*/
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
public class ApplicationPropertiesConfigOverlay {
|
||||
|
||||
private static final int MAX_DEPTH = 20;
|
||||
|
||||
@Inject ApplicationProperties applicationProperties;
|
||||
|
||||
void onStart(@Observes @Priority(Interceptor.Priority.APPLICATION) StartupEvent event) {
|
||||
Config config = ConfigProvider.getConfig();
|
||||
// ApplicationProperties is @ApplicationScoped, so the injected reference is a client proxy;
|
||||
// reflect over the real contextual instance (its getters delegate, but getDeclaredFields()
|
||||
// on the proxy would not see the model fields).
|
||||
Object root = applicationProperties;
|
||||
if (root instanceof ClientProxy proxy) {
|
||||
root = proxy.arc_contextualInstance();
|
||||
}
|
||||
int[] applied = {0};
|
||||
bind(root, "", config, 0, applied);
|
||||
log.info(
|
||||
"Applied {} configuration override(s) onto ApplicationProperties"
|
||||
+ " (settings.yml + environment)",
|
||||
applied[0]);
|
||||
}
|
||||
|
||||
private void bind(Object node, String prefix, Config config, int depth, int[] applied) {
|
||||
if (node == null || depth > MAX_DEPTH) {
|
||||
return;
|
||||
}
|
||||
for (Field field : node.getClass().getDeclaredFields()) {
|
||||
int mods = field.getModifiers();
|
||||
if (Modifier.isStatic(mods) || field.isSynthetic()) {
|
||||
continue;
|
||||
}
|
||||
String key = prefix.isEmpty() ? field.getName() : prefix + "." + field.getName();
|
||||
Class<?> type = field.getType();
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
if (isModelType(type)) {
|
||||
Object child = field.get(node);
|
||||
if (child == null) {
|
||||
child = instantiate(type);
|
||||
if (child != null) {
|
||||
field.set(node, child);
|
||||
}
|
||||
}
|
||||
bind(child, key, config, depth + 1, applied);
|
||||
} else if (List.class.isAssignableFrom(type)) {
|
||||
Class<?> element = listElementType(field);
|
||||
if (element != null && isLeaf(element)) {
|
||||
config.getOptionalValues(key, element)
|
||||
.ifPresent(value -> apply(field, node, value, key, applied));
|
||||
}
|
||||
// List<model-type> has no flat scalar representation here - skip.
|
||||
} else if (isLeaf(type)) {
|
||||
config.getOptionalValue(key, box(type))
|
||||
.ifPresent(value -> apply(field, node, value, key, applied));
|
||||
}
|
||||
// Maps and other container/unsupported types are left to their Java defaults.
|
||||
} catch (Exception ex) {
|
||||
// Per-field best effort: an unconvertible value or inaccessible field must not
|
||||
// abort
|
||||
// the whole overlay. Never include the value (may be a secret).
|
||||
log.debug("Skipped config binding for {} ({})", key, ex.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void apply(Field field, Object node, Object value, String key, int[] applied) {
|
||||
try {
|
||||
field.set(node, value);
|
||||
applied[0]++;
|
||||
// Key name only - the value may be a secret (license key, password, client secret).
|
||||
log.debug("Applied config override: {}", key);
|
||||
} catch (Exception ex) {
|
||||
log.debug("Failed to set {} ({})", key, ex.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isModelType(Class<?> type) {
|
||||
return type.getName().startsWith("stirling.software") && !type.isEnum();
|
||||
}
|
||||
|
||||
private static boolean isLeaf(Class<?> type) {
|
||||
return type == String.class
|
||||
|| type.isEnum()
|
||||
|| type.isPrimitive()
|
||||
|| type == Boolean.class
|
||||
|| type == Integer.class
|
||||
|| type == Long.class
|
||||
|| type == Double.class
|
||||
|| type == Float.class
|
||||
|| type == Short.class
|
||||
|| type == Byte.class;
|
||||
}
|
||||
|
||||
private static Class<?> box(Class<?> type) {
|
||||
if (!type.isPrimitive()) {
|
||||
return type;
|
||||
}
|
||||
if (type == boolean.class) {
|
||||
return Boolean.class;
|
||||
}
|
||||
if (type == int.class) {
|
||||
return Integer.class;
|
||||
}
|
||||
if (type == long.class) {
|
||||
return Long.class;
|
||||
}
|
||||
if (type == double.class) {
|
||||
return Double.class;
|
||||
}
|
||||
if (type == float.class) {
|
||||
return Float.class;
|
||||
}
|
||||
if (type == short.class) {
|
||||
return Short.class;
|
||||
}
|
||||
if (type == byte.class) {
|
||||
return Byte.class;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private static Class<?> listElementType(Field field) {
|
||||
Type generic = field.getGenericType();
|
||||
if (generic instanceof ParameterizedType parameterized) {
|
||||
Type[] args = parameterized.getActualTypeArguments();
|
||||
if (args.length == 1 && args[0] instanceof Class<?> element) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object instantiate(Class<?> type) {
|
||||
try {
|
||||
return type.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,29 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import com.posthog.java.PostHog;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Configuration
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class PostHogConfig {
|
||||
|
||||
@Value("${posthog.api.key}")
|
||||
private String posthogApiKey;
|
||||
@ConfigProperty(name = "posthog.api.key")
|
||||
String posthogApiKey;
|
||||
|
||||
@Value("${posthog.host}")
|
||||
private String posthogHost;
|
||||
@ConfigProperty(name = "posthog.host")
|
||||
String posthogHost;
|
||||
|
||||
private PostHog postHogClient;
|
||||
|
||||
@Bean
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
public PostHog postHogClient() {
|
||||
postHogClient =
|
||||
new PostHog.Builder(posthogApiKey)
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.posthog.java.PostHogLogger;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@ApplicationScoped
|
||||
public class PostHogLoggerImpl implements PostHogLogger {
|
||||
|
||||
@Override
|
||||
|
||||
+3
-2
@@ -10,7 +10,8 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -24,7 +25,7 @@ import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.UnoServerPool;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ApplicationScoped
|
||||
@Getter
|
||||
public class RuntimePathConfig {
|
||||
private final ApplicationProperties properties;
|
||||
|
||||
+8
-16
@@ -1,23 +1,15 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
|
||||
|
||||
/**
|
||||
* Configures the scheduler used by all {@code @Scheduled} methods. Uses virtual threads so that
|
||||
* long-running scheduled tasks (e.g. cleanup, license checks, file monitoring) never block each
|
||||
* other — each runs on its own lightweight virtual thread.
|
||||
*
|
||||
* <p>MIGRATION (Spring -> Quarkus): the custom Spring {@code TaskScheduler} bean has been removed.
|
||||
* 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).
|
||||
*/
|
||||
@Configuration
|
||||
public class SchedulingConfig {
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
|
||||
scheduler.setVirtualThreads(true);
|
||||
scheduler.setThreadNamePrefix("scheduled-vt-");
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
public class SchedulingConfig {}
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.microprofile.config.spi.ConfigSource;
|
||||
import org.snakeyaml.engine.v2.api.Load;
|
||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||
|
||||
/**
|
||||
* Exposes {@code settings.yml} (and {@code custom_settings.yml}, with the bundled {@code
|
||||
* settings.yml.template} as the default fallback) as a MicroProfile/SmallRye {@link ConfigSource}.
|
||||
*
|
||||
* <p>Restores the Spring {@code @ConfigurationProperties} behaviour that bound {@code settings.yml}
|
||||
* into {@code ApplicationProperties}: without this the YAML was never read under Quarkus, so flags
|
||||
* like {@code security.enableLogin} fell back to their Java defaults regardless of the file (the
|
||||
* {@code enableLogin=false}/{@code maxDPI=0}/{@code loginAttemptCount=0} class of bugs). The nested
|
||||
* YAML is flattened to dotted keys ({@code security.enableLogin -> "true"}); {@link
|
||||
* ApplicationPropertiesConfigOverlay} and {@code @ConfigProperty} injections then read them.
|
||||
*
|
||||
* <p>Ordinal {@value #ORDINAL} sits above {@code application.properties} (250) but below
|
||||
* environment variables (300) and system properties (400), matching Spring's precedence - e.g.
|
||||
* {@code SECURITY_ENABLELOGIN} still overrides the file.
|
||||
*
|
||||
* <p>Registered via {@code META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource}.
|
||||
*/
|
||||
public class SettingsYamlConfigSource implements ConfigSource {
|
||||
|
||||
private static final int ORDINAL = 275;
|
||||
|
||||
private final Map<String, String> properties;
|
||||
|
||||
public SettingsYamlConfigSource() {
|
||||
this.properties = load();
|
||||
}
|
||||
|
||||
private static Map<String, String> load() {
|
||||
Map<String, String> flat = new HashMap<>();
|
||||
// 1. Bundled template provides the defaults (e.g. security.enableLogin: true).
|
||||
try (InputStream in =
|
||||
SettingsYamlConfigSource.class
|
||||
.getClassLoader()
|
||||
.getResourceAsStream("settings.yml.template")) {
|
||||
if (in != null) {
|
||||
flatten("", loadYaml(in), flat);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// best effort - fall through to file overrides / Java defaults
|
||||
}
|
||||
// 2. The user's settings.yml overrides the template.
|
||||
mergeFile(InstallationPathConfig.getSettingsPath(), flat);
|
||||
// 3. custom_settings.yml overrides settings.yml.
|
||||
mergeFile(InstallationPathConfig.getCustomSettingsPath(), flat);
|
||||
return flat;
|
||||
}
|
||||
|
||||
private static void mergeFile(String path, Map<String, String> flat) {
|
||||
try {
|
||||
Path p = Path.of(path);
|
||||
if (Files.isRegularFile(p)) {
|
||||
try (InputStream in = Files.newInputStream(p)) {
|
||||
flatten("", loadYaml(in), flat);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// unreadable/invalid file - keep whatever defaults were already loaded
|
||||
}
|
||||
}
|
||||
|
||||
private static Object loadYaml(InputStream in) {
|
||||
return new Load(LoadSettings.builder().build()).loadFromInputStream(in);
|
||||
}
|
||||
|
||||
private static void flatten(String prefix, Object node, Map<String, String> out) {
|
||||
if (node instanceof Map<?, ?> map) {
|
||||
for (Map.Entry<?, ?> e : map.entrySet()) {
|
||||
String key =
|
||||
prefix.isEmpty() ? String.valueOf(e.getKey()) : prefix + "." + e.getKey();
|
||||
flatten(key, e.getValue(), out);
|
||||
}
|
||||
} else if (node instanceof List<?> list) {
|
||||
// Emit scalar lists as a comma-separated value so SmallRye binds them via
|
||||
// config.getValues()/getOptionalValues() (e.g. endpoints.toRemove, consumed by
|
||||
// EndpointConfiguration to disable endpoints). Lists containing maps/nested lists have
|
||||
// no
|
||||
// flat scalar form, so skip those - their consumers read them structurally, not through
|
||||
// this overlay. The scalar lists here (endpoint names, group names) contain no commas,
|
||||
// so
|
||||
// a plain join round-trips cleanly.
|
||||
boolean scalarList =
|
||||
!list.isEmpty()
|
||||
&& list.stream()
|
||||
.allMatch(
|
||||
e ->
|
||||
e != null
|
||||
&& !(e instanceof Map)
|
||||
&& !(e instanceof List));
|
||||
if (scalarList) {
|
||||
out.put(
|
||||
prefix,
|
||||
list.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(java.util.stream.Collectors.joining(",")));
|
||||
}
|
||||
return;
|
||||
} else if (node != null) {
|
||||
out.put(prefix, String.valueOf(node));
|
||||
}
|
||||
// null leaves are left unset so the Java default applies.
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPropertyNames() {
|
||||
return properties.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(String propertyName) {
|
||||
return properties.get(propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "settings.yml";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrdinal() {
|
||||
return ORDINAL;
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
import org.springframework.core.io.support.PropertySourceFactory;
|
||||
|
||||
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||
|
||||
@Override
|
||||
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) {
|
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||
factory.setResources(encodedResource.getResource());
|
||||
Properties properties = factory.getObject();
|
||||
|
||||
return new PropertiesPropertySource(
|
||||
encodedResource.getResource().getFilename(), properties);
|
||||
}
|
||||
}
|
||||
+23
-53
@@ -1,7 +1,6 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
@@ -15,22 +14,11 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
@@ -39,9 +27,11 @@ import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.YamlPropertySourceFactory;
|
||||
import stirling.software.common.constants.JwtConstants;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.common.model.io.ClassPathResource;
|
||||
import stirling.software.common.model.io.FileSystemResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||
import stirling.software.common.model.oauth2.KeycloakProvider;
|
||||
@@ -51,9 +41,7 @@ import stirling.software.common.util.ValidationUtils;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@ConfigurationProperties(prefix = "")
|
||||
@ApplicationScoped
|
||||
public class ApplicationProperties {
|
||||
|
||||
private Legal legal = new Legal();
|
||||
@@ -82,38 +70,12 @@ public class ApplicationProperties {
|
||||
private Cluster cluster = new Cluster();
|
||||
private Policies policies = new Policies();
|
||||
|
||||
@Bean
|
||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||
throws IOException {
|
||||
String configPath = InstallationPathConfig.getSettingsPath();
|
||||
log.debug("Attempting to load settings from: {}", configPath);
|
||||
|
||||
File file = new File(configPath);
|
||||
if (!file.exists()) {
|
||||
log.error("Warning: Settings file does not exist at: {}", configPath);
|
||||
}
|
||||
|
||||
Resource resource = new FileSystemResource(configPath);
|
||||
if (!resource.exists()) {
|
||||
throw new FileNotFoundException("Settings file not found at: " + configPath);
|
||||
}
|
||||
|
||||
EncodedResource encodedResource = new EncodedResource(resource);
|
||||
PropertySource<?> propertySource =
|
||||
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
||||
|
||||
boolean saasActive = Arrays.asList(environment.getActiveProfiles()).contains("saas");
|
||||
if (saasActive) {
|
||||
// Saas-pinned values in application-saas.properties must beat settings.yml.
|
||||
environment.getPropertySources().addLast(propertySource);
|
||||
} else {
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
}
|
||||
|
||||
log.debug("Loaded properties: {}", propertySource.getSource());
|
||||
|
||||
return propertySource;
|
||||
}
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Initialize fileUploadLimit from environment variables if not set in settings.yml. Supports
|
||||
@@ -673,8 +635,12 @@ public class ApplicationProperties {
|
||||
private InitialLogin initialLogin = new InitialLogin();
|
||||
private OAUTH2 oauth2 = new OAUTH2();
|
||||
private SAML2 saml2 = new SAML2();
|
||||
private int loginAttemptCount;
|
||||
private long loginResetTimeMinutes;
|
||||
// Defaults mirror settings.yml.template. These primitives are not bound from the template
|
||||
// by the current Quarkus config path, so an unset 0 means "lock after 0 attempts" (every
|
||||
// login blocked, and the lockout never accumulates a window) - same class of bug as
|
||||
// maxDPI=0. See the settings.yml binding TODO.
|
||||
private int loginAttemptCount = 5;
|
||||
private long loginResetTimeMinutes = 120;
|
||||
private String loginMethod = "all";
|
||||
private String customGlobalAPIKey;
|
||||
private Jwt jwt = new Jwt();
|
||||
@@ -759,8 +725,9 @@ public class ApplicationProperties {
|
||||
@JsonIgnore
|
||||
public InputStream getIdpMetadataUri() throws IOException {
|
||||
if (idpMetadataUri.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpMetadataUri.substring("classpath:".length()))
|
||||
.getInputStream();
|
||||
return getClass()
|
||||
.getClassLoader()
|
||||
.getResourceAsStream(idpMetadataUri.substring("classpath:".length()));
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(idpMetadataUri);
|
||||
@@ -1031,6 +998,9 @@ public class ApplicationProperties {
|
||||
private Boolean enableDesktopInstallSlide = true;
|
||||
private Datasource datasource;
|
||||
private boolean disableSanitize;
|
||||
// Default mirrors settings.yml.template (maxDPI: 500). Without an explicit default this
|
||||
// primitive is 0, which makes every DPI check (dpi > maxDPI) fail with "maximum safe limit
|
||||
// of 0" when the value is not populated from settings.
|
||||
private int maxDPI = 500;
|
||||
private boolean enableUrlToPDF;
|
||||
private Html html = new Html();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import stirling.software.common.model.io.Resource;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for Spring's {@code
|
||||
* org.springframework.web.multipart.MultipartFile}.
|
||||
*
|
||||
* <p>Quarkus/JAX-RS has no drop-in equivalent for the {@code MultipartFile} abstraction that the
|
||||
* 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.software.common.model.multipart.FileUploadMultipartFile}) and pass it down
|
||||
* unchanged.
|
||||
*/
|
||||
public interface MultipartFile {
|
||||
|
||||
String getName();
|
||||
|
||||
String getOriginalFilename();
|
||||
|
||||
String getContentType();
|
||||
|
||||
boolean isEmpty();
|
||||
|
||||
long getSize();
|
||||
|
||||
byte[] getBytes() throws IOException;
|
||||
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
/**
|
||||
* The content as a {@link Resource}. The default is a stream-backed resource; file-backed
|
||||
* implementations (e.g. {@code FileUploadMultipartFile}) override this to enable zero-copy fast
|
||||
* paths.
|
||||
*/
|
||||
default Resource getResource() {
|
||||
try {
|
||||
return new stirling.software.common.model.io.InputStreamResource(
|
||||
getInputStream(), getOriginalFilename());
|
||||
} catch (IOException e) {
|
||||
throw new java.io.UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
default void transferTo(File dest) throws IOException {
|
||||
transferTo(dest.toPath());
|
||||
}
|
||||
|
||||
default void transferTo(Path dest) throws IOException {
|
||||
try (InputStream in = getInputStream()) {
|
||||
// Spring's MultipartFile#transferTo overwrites an existing destination. Callers
|
||||
// commonly
|
||||
// pass a path from Files.createTempFile(...) (which has already created an empty file),
|
||||
// so REPLACE_EXISTING is required - a plain Files.copy would throw FileAlreadyExists.
|
||||
Files.copy(in, dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package stirling.software.common.model.api;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class GeneralFile {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package stirling.software.common.model.api;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
@@ -11,6 +8,8 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
@@ -18,7 +17,7 @@ public class PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "The input PDF file",
|
||||
contentMediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||
contentMediaType = "application/pdf",
|
||||
format = "binary")
|
||||
private MultipartFile fileInput;
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package stirling.software.common.model.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* In-memory byte-backed {@link Resource} (migration shim for Spring's {@code ByteArrayResource}).
|
||||
*
|
||||
* <p>Unlike {@link InputStreamResource} this is re-readable: every {@link #getInputStream()} hands
|
||||
* back a fresh stream over the same bytes. Callers that hold one resource across several consumers
|
||||
* - a policy run passes its supporting files to every step - depend on that.
|
||||
*/
|
||||
public class ByteArrayResource implements Resource {
|
||||
|
||||
private final byte[] byteArray;
|
||||
private final String filename;
|
||||
|
||||
public ByteArrayResource(byte[] byteArray) {
|
||||
this(byteArray, null);
|
||||
}
|
||||
|
||||
public ByteArrayResource(byte[] byteArray, String filename) {
|
||||
this.byteArray = byteArray == null ? new byte[0] : byteArray;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
/** The backing bytes. Not defensively copied, matching Spring's contract. */
|
||||
public byte[] getByteArray() {
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream(byteArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return byteArray.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
throw new IOException("ByteArrayResource is not backed by a file");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package stirling.software.common.model.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
/** Classpath-backed {@link Resource} (migration shim for Spring's {@code ClassPathResource}). */
|
||||
public class ClassPathResource implements Resource {
|
||||
|
||||
private final String path;
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
public ClassPathResource(String path) {
|
||||
this(path, ClassPathResource.class.getClassLoader());
|
||||
}
|
||||
|
||||
public ClassPathResource(String path, ClassLoader classLoader) {
|
||||
this.path = path.startsWith("/") ? path.substring(1) : path;
|
||||
this.classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
InputStream is = classLoader.getResourceAsStream(path);
|
||||
if (is == null) {
|
||||
throw new IOException("class path resource [" + path + "] cannot be opened");
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return classLoader.getResource(path) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
int sep = path.lastIndexOf('/');
|
||||
return sep != -1 ? path.substring(sep + 1) : path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
try (InputStream is = getInputStream()) {
|
||||
long count = 0;
|
||||
byte[] buf = new byte[8192];
|
||||
int read;
|
||||
while ((read = is.read(buf)) != -1) {
|
||||
count += read;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
URL url = classLoader.getResource(path);
|
||||
if (url == null || !"file".equals(url.getProtocol())) {
|
||||
throw new IOException("class path resource [" + path + "] is not a filesystem file");
|
||||
}
|
||||
return new File(url.getFile());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package stirling.software.common.model.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/** File-backed {@link Resource} (migration shim for Spring's {@code FileSystemResource}). */
|
||||
public class FileSystemResource implements Resource {
|
||||
|
||||
private final Path path;
|
||||
|
||||
public FileSystemResource(Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public FileSystemResource(File file) {
|
||||
this.path = file.toPath();
|
||||
}
|
||||
|
||||
public FileSystemResource(String path) {
|
||||
this.path = Path.of(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return Files.newInputStream(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return Files.exists(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
Path name = path.getFileName();
|
||||
return name == null ? null : name.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return Files.size(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() {
|
||||
return path.toFile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package stirling.software.common.model.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Stream-backed {@link Resource} (migration shim for Spring's {@code InputStreamResource}). As with
|
||||
* Spring, the stream can only be read once.
|
||||
*/
|
||||
public class InputStreamResource implements Resource {
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final String filename;
|
||||
|
||||
public InputStreamResource(InputStream inputStream) {
|
||||
this(inputStream, null);
|
||||
}
|
||||
|
||||
public InputStreamResource(InputStream inputStream, String filename) {
|
||||
this.inputStream = inputStream;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
// Spring's InputStreamResource also cannot report length without consuming the stream.
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
throw new IOException("InputStreamResource is not backed by a file");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package stirling.software.common.model.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for Spring's {@code org.springframework.core.io.Resource}.
|
||||
*
|
||||
* <p>Quarkus/Jakarta has no single {@code Resource} abstraction. Rather than rewrite the many
|
||||
* 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/isOpen}) together with the {@link
|
||||
* FileSystemResource}, {@link InputStreamResource}, {@link ByteArrayResource} and {@link
|
||||
* ClassPathResource} implementations. Converting a file is then just an import swap.
|
||||
*/
|
||||
public interface Resource {
|
||||
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
boolean exists();
|
||||
|
||||
String getFilename();
|
||||
|
||||
long contentLength() throws IOException;
|
||||
|
||||
/** Whether this resource is backed by a real file in the filesystem. */
|
||||
default boolean isFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this resource wraps an already-open stream, so {@link #getInputStream()} can only be
|
||||
* read once and must be consumed or closed to avoid a leak.
|
||||
*/
|
||||
default boolean isOpen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the underlying file.
|
||||
* @throws IOException if the resource is not file-backed.
|
||||
*/
|
||||
File getFile() throws IOException;
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package stirling.software.common.model.multipart;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
import stirling.software.common.model.io.InputStreamResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
|
||||
/**
|
||||
* In-memory {@link MultipartFile} backed by a byte array. Useful for tests and for code paths that
|
||||
* synthesize file content (migration shim - see {@link MultipartFile}).
|
||||
*/
|
||||
public class ByteArrayMultipartFile implements MultipartFile {
|
||||
|
||||
private final String name;
|
||||
private final String originalFilename;
|
||||
private final String contentType;
|
||||
private final byte[] content;
|
||||
|
||||
public ByteArrayMultipartFile(
|
||||
String name, String originalFilename, String contentType, byte[] content) {
|
||||
this.name = name;
|
||||
this.originalFilename = originalFilename;
|
||||
this.contentType = contentType;
|
||||
this.content = content != null ? content : new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return originalFilename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return content.length == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return content.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getResource() {
|
||||
return new InputStreamResource(new ByteArrayInputStream(content), originalFilename);
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package stirling.software.common.model.multipart;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||
|
||||
import stirling.software.common.model.MultipartFile;
|
||||
import stirling.software.common.model.io.FileSystemResource;
|
||||
import stirling.software.common.model.io.Resource;
|
||||
|
||||
/**
|
||||
* Adapts a Quarkus REST {@link FileUpload} (the inbound multipart representation at the JAX-RS
|
||||
* boundary) to the {@link MultipartFile} migration shim, so controllers can pass uploads down to
|
||||
* the existing service layer without changing its method signatures.
|
||||
*/
|
||||
public class FileUploadMultipartFile implements MultipartFile {
|
||||
|
||||
private final FileUpload delegate;
|
||||
|
||||
public FileUploadMultipartFile(FileUpload delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/** Null-safe factory: returns null when the upload is absent. */
|
||||
public static MultipartFile of(FileUpload upload) {
|
||||
return upload == null ? null : new FileUploadMultipartFile(upload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Null-safe factory for a multipart field that may have multiple parts under the same name.
|
||||
*
|
||||
* <p>Spring's MultipartFile binding picked the actual file part even when a client also sent a
|
||||
* plain text form field of the same name; RESTEasy Reactive's {@code @RestForm FileUpload}
|
||||
* binds the <em>first</em> part by name instead, so a stray {@code name=value} text part sent
|
||||
* before the file would shadow the upload. Prefer the part that carries a real filename (the
|
||||
* file), falling back to the last part, so such requests bind the same way they did under
|
||||
* Spring.
|
||||
*/
|
||||
public static MultipartFile of(List<FileUpload> uploads) {
|
||||
if (uploads == null || uploads.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
FileUpload chosen = null;
|
||||
for (FileUpload upload : uploads) {
|
||||
if (upload.fileName() != null && !upload.fileName().isBlank()) {
|
||||
chosen = upload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chosen == null) {
|
||||
chosen = uploads.get(uploads.size() - 1);
|
||||
}
|
||||
return new FileUploadMultipartFile(chosen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return delegate.fileName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return delegate.contentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return getSize() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() throws IOException {
|
||||
return Files.readAllBytes(delegate.uploadedFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return Files.newInputStream(delegate.uploadedFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource getResource() {
|
||||
// File-backed: enables FileStorage's zero-copy fast path.
|
||||
return new FileSystemResource(delegate.uploadedFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferTo(Path dest) throws IOException {
|
||||
// Overwrite semantics like Spring's MultipartFile#transferTo; callers often pass a
|
||||
// Files.createTempFile(...) path that already exists, so REPLACE_EXISTING is required.
|
||||
Files.copy(
|
||||
delegate.uploadedFile(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.authentication.AbstractAuthenticationToken}.
|
||||
*
|
||||
* <p>Base implementation of {@link Authentication} holding authorities, details and an
|
||||
* authenticated flag.
|
||||
*/
|
||||
public abstract class AbstractAuthenticationToken implements Authentication {
|
||||
|
||||
private final List<GrantedAuthority> authorities;
|
||||
private Object details;
|
||||
private boolean authenticated = false;
|
||||
|
||||
protected AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
|
||||
if (authorities == null) {
|
||||
this.authorities = Collections.emptyList();
|
||||
} else {
|
||||
List<GrantedAuthority> copy = new ArrayList<>(authorities.size());
|
||||
for (GrantedAuthority authority : authorities) {
|
||||
copy.add(authority);
|
||||
}
|
||||
this.authorities = Collections.unmodifiableList(copy);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void setDetails(Object details) {
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthenticated() {
|
||||
return authenticated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
|
||||
this.authenticated = authenticated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
Object principal = getPrincipal();
|
||||
if (principal instanceof UserDetails) {
|
||||
return ((UserDetails) principal).getUsername();
|
||||
}
|
||||
return principal == null ? null : principal.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code org.springframework.security.core.Authentication}.
|
||||
*
|
||||
* <p>Represents the token for an authentication request or for an authenticated principal once the
|
||||
* request has been processed.
|
||||
*/
|
||||
public interface Authentication extends Principal {
|
||||
|
||||
Collection<? extends GrantedAuthority> getAuthorities();
|
||||
|
||||
Object getCredentials();
|
||||
|
||||
Object getDetails();
|
||||
|
||||
Object getPrincipal();
|
||||
|
||||
boolean isAuthenticated();
|
||||
|
||||
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
|
||||
|
||||
@Override
|
||||
String getName();
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.core.AuthenticationException}.
|
||||
*
|
||||
* <p>Abstract superclass for all exceptions related to an {@link Authentication} object being
|
||||
* invalid for whatever reason.
|
||||
*/
|
||||
public class AuthenticationException extends RuntimeException {
|
||||
|
||||
public AuthenticationException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public AuthenticationException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
import at.favre.lib.crypto.bcrypt.BCrypt;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder}.
|
||||
*
|
||||
* <p>Implementation of {@link PasswordEncoder} backed by the {@code at.favre.lib:bcrypt} library.
|
||||
*/
|
||||
public class BCryptPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
private static final int DEFAULT_STRENGTH = 10;
|
||||
|
||||
private final int strength;
|
||||
|
||||
public BCryptPasswordEncoder() {
|
||||
this(DEFAULT_STRENGTH);
|
||||
}
|
||||
|
||||
public BCryptPasswordEncoder(int strength) {
|
||||
this.strength = strength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
return BCrypt.withDefaults().hashToString(strength, rawPassword.toString().toCharArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
if (rawPassword == null) {
|
||||
throw new IllegalArgumentException("rawPassword cannot be null");
|
||||
}
|
||||
if (encodedPassword == null || encodedPassword.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return BCrypt.verifyer()
|
||||
.verify(rawPassword.toString().toCharArray(), encodedPassword)
|
||||
.verified;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.authentication.BadCredentialsException}.
|
||||
*
|
||||
* <p>Thrown if an authentication request is rejected because the credentials are invalid.
|
||||
*/
|
||||
public class BadCredentialsException extends AuthenticationException {
|
||||
|
||||
public BadCredentialsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public BadCredentialsException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code org.springframework.security.core.GrantedAuthority}.
|
||||
*
|
||||
* <p>Represents an authority granted to an {@link Authentication} object. Provided so that code
|
||||
* migrated from Spring Boot to Quarkus compiles without Spring Security on the classpath.
|
||||
*/
|
||||
public interface GrantedAuthority {
|
||||
|
||||
/**
|
||||
* Returns a textual representation of the granted authority.
|
||||
*
|
||||
* @return the authority string, never {@code null}
|
||||
*/
|
||||
String getAuthority();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.oauth2.core.user.OAuth2User}.
|
||||
*
|
||||
* <p>Represents a user {@link java.security.Principal} authenticated using OAuth 2.0 or OpenID
|
||||
* Connect.
|
||||
*/
|
||||
public interface OAuth2User {
|
||||
|
||||
Map<String, Object> getAttributes();
|
||||
|
||||
Collection<? extends GrantedAuthority> getAuthorities();
|
||||
|
||||
String getName();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.crypto.password.PasswordEncoder}.
|
||||
*
|
||||
* <p>Service interface for encoding passwords.
|
||||
*/
|
||||
public interface PasswordEncoder {
|
||||
|
||||
/** Encodes the raw password. */
|
||||
String encode(CharSequence rawPassword);
|
||||
|
||||
/** Verifies that the encoded password matches the raw password after it too is encoded. */
|
||||
boolean matches(CharSequence rawPassword, String encodedPassword);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken}.
|
||||
*
|
||||
* <p>Holds the persistent remember-me token data for a single series.
|
||||
*/
|
||||
public class PersistentRememberMeToken {
|
||||
|
||||
private final String username;
|
||||
private final String series;
|
||||
private final String tokenValue;
|
||||
private final Date date;
|
||||
|
||||
public PersistentRememberMeToken(String username, String series, String tokenValue, Date date) {
|
||||
this.username = username;
|
||||
this.series = series;
|
||||
this.tokenValue = tokenValue;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.web.authentication.rememberme.PersistentTokenRepository}.
|
||||
*
|
||||
* <p>Persists the remember-me tokens used by the persistent token based remember-me services.
|
||||
*/
|
||||
public interface PersistentTokenRepository {
|
||||
|
||||
void createNewToken(PersistentRememberMeToken token);
|
||||
|
||||
void updateToken(String series, String tokenValue, Date lastUsed);
|
||||
|
||||
PersistentRememberMeToken getTokenForSeries(String seriesId);
|
||||
|
||||
void removeUserTokens(String username);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.core.context.SecurityContext}.
|
||||
*
|
||||
* <p>Holds the {@link Authentication} associated with the current execution.
|
||||
*/
|
||||
public interface SecurityContext {
|
||||
|
||||
Authentication getAuthentication();
|
||||
|
||||
void setAuthentication(Authentication authentication);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.core.context.SecurityContextHolder}.
|
||||
*
|
||||
* <p>Associates a {@link SecurityContext} with the current thread of execution using a {@link
|
||||
* ThreadLocal}.
|
||||
*/
|
||||
public final class SecurityContextHolder {
|
||||
|
||||
private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new ThreadLocal<>();
|
||||
|
||||
private SecurityContextHolder() {}
|
||||
|
||||
/** Returns the context for the current thread, creating an empty one if none is set. */
|
||||
public static SecurityContext getContext() {
|
||||
SecurityContext context = CONTEXT_HOLDER.get();
|
||||
if (context == null) {
|
||||
context = createEmptyContext();
|
||||
CONTEXT_HOLDER.set(context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
public static void setContext(SecurityContext context) {
|
||||
CONTEXT_HOLDER.set(context);
|
||||
}
|
||||
|
||||
public static void clearContext() {
|
||||
CONTEXT_HOLDER.remove();
|
||||
}
|
||||
|
||||
public static SecurityContext createEmptyContext() {
|
||||
return new SecurityContextImpl();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package stirling.software.common.security;
|
||||
|
||||
/**
|
||||
* Migration compatibility shim for {@code
|
||||
* org.springframework.security.core.context.SecurityContextImpl}.
|
||||
*
|
||||
* <p>Basic concrete implementation of {@link SecurityContext}.
|
||||
*/
|
||||
public class SecurityContextImpl implements SecurityContext {
|
||||
|
||||
private Authentication authentication;
|
||||
|
||||
public SecurityContextImpl() {}
|
||||
|
||||
public SecurityContextImpl(Authentication authentication) {
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication getAuthentication() {
|
||||
return authentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthentication(Authentication authentication) {
|
||||
this.authentication = authentication;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user