Files
Stirling-PDF/build.gradle
brios 1447ed8b62 refactor(redact): replace PDFBox-based text redaction with JPDFium (#7364)
# Description of Changes

Refactors automatic text redaction to use JPDFium-based redaction/text
removal instead of PDFBox

Changes:

* The `RedactController` now uses the JPDFium native redaction engine
(`PdfRedactor.redact`) as the primary method for PDF redaction, with
automatic fallback to the manual redaction service if JPDFium fails or
throws an exception. This improves reliability and leverages more robust
native features when available.
* Regex patterns provided by the user are now validated before redaction
begins, ensuring invalid patterns are rejected early with clear error
messages.
* The code now trims and filters out empty or excessively long redaction
terms, preventing unnecessary processing and potential errors.


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] 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)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have run `task check` to verify linters, typechecks, and tests
pass
- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing)
for more details.
2026-08-13 11:10:10 +01:00

818 lines
31 KiB
Groovy

plugins {
id "java"
id "jacoco"
id "io.spring.dependency-management" version "1.1.7"
id "org.springframework.boot" version "4.0.6"
id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
id "io.swagger.swaggerhub" version "1.3.2"
id "com.diffplug.spotless" version "8.8.0"
id "com.github.jk1.dependency-license-report"
//id "nebula.lint" version "19.0.3"
id "org.sonarqube" version "7.3.1.8318"
}
import com.github.jk1.license.render.*
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.xml.XmlSlurper
import org.gradle.api.JavaVersion
import org.gradle.api.tasks.testing.Test
import org.gradle.jvm.toolchain.JavaLanguageVersion
import stirling.software.gradle.ModuleLicenseOverrideFilter
ext {
springBootVersion = "4.0.6"
pdfboxVersion = "3.0.8"
imageioVersion = "3.13.1"
lombokVersion = "1.18.46"
bouncycastleVersion = "1.85"
springSecuritySamlVersion = "7.1.0"
openSamlVersion = "5.2.1"
commonmarkVersion = "0.28.0"
googleJavaFormatVersion = "1.35.0"
logback = "1.5.32"
commonsIoVersion = "2.22.0"
commonsLang3 = "3.20.0"
rhinoVersion = "1.9.1"
okhttpBomVersion = "5.3.2"
gsonVersion = "2.14.0"
guavaVersion = "33.6.0-jre"
jinjavaVersion = "2.8.4"
jackson2Version = "2.22.1"
bucket4jVersion = "8.19.0"
archunitVersion = "1.4.2"
batikVersion = "1.19"
jpdfiumVersion = "1.0.4"
jwtVersion = "0.13.0"
awsSdkVersion = "2.44.12"
jschVersion = "0.2.23"
commonsNetVersion = "3.11.1"
smbjVersion = "0.14.0"
tinkVersion = "1.23.0"
testcontainersMinioVersion = "1.21.4"
// junit-platform-launcher version managed by Spring Boot BOM
modernJavaVersion = 25
}
def buildJavaMajorVersion = (project.findProperty('javaVersion') ?: ext.modernJavaVersion).toString().toInteger()
def buildJavaLanguageVersion = JavaLanguageVersion.of(buildJavaMajorVersion)
def buildJavaVersion = JavaVersion.toVersion(buildJavaMajorVersion.toString())
java {
sourceCompatibility = buildJavaVersion
targetCompatibility = buildJavaVersion
toolchain {
languageVersion = buildJavaLanguageVersion
}
}
ext.isSecurityDisabled = { ->
System.getenv('DOCKER_ENABLE_SECURITY') == 'false' ||
System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true' ||
(project.hasProperty('DISABLE_ADDITIONAL_FEATURES') &&
System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')
}
ext.mavenUrl = System.getenv("MAVEN_PUBLIC_URL") ?: ""
ext.username = System.getenv('MAVEN_USER') ?: ""
ext.password = System.getenv('MAVEN_PASSWORD') ?: ""
if (rootProject.ext.mavenUrl.isEmpty()) {
println "No custom MAVEN_PUBLIC_URL set, defaulting to Maven Central"
} else {
println "MAVEN_PUBLIC_URL set"
}
jar {
enabled = false
manifest {
attributes "Implementation-Title": "Stirling-PDF",
"Implementation-Version": project.version
}
}
bootJar {
enabled = false
}
// :saas is only included for SaaS builds, but a default-flavor clean must still remove artifacts
// left behind by an earlier SaaS build.
tasks.named('clean') {
delete layout.projectDirectory.dir('app/saas/build')
}
// Configure main class for the root project
springBoot {
mainClass = 'stirling.software.SPDF.SPDFApplication'
}
allprojects {
group = 'stirling.software'
version = '2.14.2'
configurations.configureEach {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
}
}
def appVersionStr = project.version.toString()
def tauriConfigPath = layout.projectDirectory.file('frontend/editor/src-tauri/tauri.conf.json').asFile.path
def sim1Path = layout.projectDirectory.file('frontend/editor/src/core/testing/serverExperienceSimulations.ts').asFile.path
def sim2Path = layout.projectDirectory.file('frontend/editor/src/proprietary/testing/serverExperienceSimulations.ts').asFile.path
def aurDesktopPkgbuildPath = layout.projectDirectory.file('.github/aur/stirling-pdf-desktop/PKGBUILD').asFile.path
def aurServerPkgbuildPath = layout.projectDirectory.file('.github/aur/stirling-pdf-server-bin/PKGBUILD').asFile.path
tasks.register('syncAppVersion') {
group = 'versioning'
description = 'Synchronizes app version across desktop, simulation, and AUR PKGBUILD configs.'
doLast {
println "Synchronizing application version to ${appVersionStr}"
def tauriConfigFile = new File(tauriConfigPath)
if (tauriConfigFile.exists()) {
def content = tauriConfigFile.getText('UTF-8')
def matcher = (content =~ /(?m)^(\s*"version":\s*")([^"]*)(")/)
if (!matcher.find()) {
throw new GradleException("Could not locate version in ${tauriConfigFile} for synchronization")
}
def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(3)}")
if (content != updatedContent) {
tauriConfigFile.write(updatedContent, 'UTF-8')
}
}
[new File(sim1Path), new File(sim2Path)].each { f ->
if (f.exists()) {
def content = f.getText('UTF-8')
def matcher = (content =~ /(appVersion:\s*(['"]))(.*?)(\2)/)
if (!matcher.find()) {
throw new GradleException("Could not locate appVersion in ${f} for synchronization")
}
def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(4)}")
if (content != updatedContent) {
f.write(updatedContent, 'UTF-8')
}
}
}
[new File(aurDesktopPkgbuildPath), new File(aurServerPkgbuildPath)].each { f ->
if (f.exists()) {
def content = f.getText('UTF-8')
def matcher = (content =~ /(?m)^(pkgver=)(.*)$/)
if (!matcher.find()) {
throw new GradleException("Could not locate pkgver in ${f} for synchronization")
}
def updatedContent = matcher.replaceFirst("\$1${appVersionStr}")
if (content != updatedContent) {
f.write(updatedContent, 'UTF-8')
}
}
}
}
}
tasks.register('writeVersion', WriteProperties) {
destinationFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
println "Writing version.properties to ${destinationFile.get().asFile.path}"
comment = "${new Date()}"
property 'version', project.provider { project.version.toString() }
}
subprojects {
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'com.diffplug.spotless'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'jacoco'
apply from: rootProject.file('gradle/spotless.gradle')
java {
sourceCompatibility = buildJavaVersion
targetCompatibility = buildJavaVersion
toolchain {
languageVersion = buildJavaLanguageVersion
}
}
if (project.name != "stirling-pdf") {
bootJar {
enabled = false
}
}
repositories {
if (!rootProject.ext.mavenUrl.isEmpty()) {
maven {
url = rootProject.ext.mavenUrl + '/releases'
credentials(PasswordCredentials) {
username = rootProject.ext.username
password = rootProject.ext.password
}
authentication {
basic(BasicAuthentication)
}
allowInsecureProtocol = true
}
}
// Maven Central first; mirrors below are fallbacks for niche artifacts.
mavenCentral()
maven { url = "https://repository.jboss.org/" }
maven { url = "https://build.shibboleth.net/maven/releases" }
}
configurations.configureEach {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
// Exclude vulnerable BouncyCastle version used in tableau
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
// Security CVE fixes - hardcoded resolution strategy to ensure safe versions
// Primary fixes via explicit dependencies in app/core/build.gradle:
// - CVE-2022-25647: gson 2.8.9+ (explicit dependency overrides tabula 2.8.7)
// - CVE-2025-66453: rhino 1.7.15 (explicit dependency overrides verapdf 1.7.13)
// Fallback strategy force declarations for additional safety:
resolutionStrategy.force "com.google.code.gson:gson:${gsonVersion}"
resolutionStrategy.force "org.mozilla:rhino:${rhinoVersion}"
// CVE-2025-48924: commons-lang3 3.20.0 DoS prevention
resolutionStrategy.force "org.apache.commons:commons-lang3:${commonsLang3}"
// CVE-2024-47554: commons-io DoS prevention
resolutionStrategy.force "commons-io:commons-io:${commonsIoVersion}"
// Jackson 2 is transitive-only here (jinjava, opensaml, jjwt request older versions);
// pin the family to a current release and keep modules aligned.
resolutionStrategy.force "com.fasterxml.jackson.core:jackson-core:${jackson2Version}"
resolutionStrategy.force "com.fasterxml.jackson.core:jackson-databind:${jackson2Version}"
resolutionStrategy.force "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${jackson2Version}"
resolutionStrategy.force "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${jackson2Version}"
resolutionStrategy.force "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jackson2Version}"
// Keep BouncyCastle modules aligned to avoid runtime linkage errors
resolutionStrategy.force "org.bouncycastle:bcprov-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.bouncycastle:bcpkix-jdk18on:${bouncycastleVersion}"
resolutionStrategy.force "org.bouncycastle:bcutil-jdk18on:${bouncycastleVersion}"
}
dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:$springBootVersion"
}
dependencies {
// Override BOM-managed commons-lang3 for CVE-2025-48924 fix
dependency "org.apache.commons:commons-lang3:$commonsLang3"
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.github.pixee:java-security-toolkit:1.2.3'
//tmp for security bumps
implementation "ch.qos.logback:logback-core:$logback"
implementation "ch.qos.logback:logback-classic:$logback"
compileOnly "org.projectlombok:lombok:$lombokVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
// Jackson 3 (Spring Boot 4 uses tools.jackson)
implementation 'org.springframework.boot:spring-boot-starter-jackson'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
testImplementation platform("com.squareup.okhttp3:okhttp-bom:${okhttpBomVersion}")
testImplementation "com.squareup.okhttp3:mockwebserver"
}
tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
options.release = buildJavaMajorVersion
if (!project.hasProperty("noSpotless")) {
dependsOn "spotlessApply"
}
}
tasks.named("compileJava", JavaCompile).configure {
// options.compilerArgs.add("-Xlint:deprecation")
// options.compilerArgs.add("-Xlint:unchecked")
}
def jacocoReport = tasks.named("jacocoTestReport")
tasks.withType(Test).configureEach {
useJUnitPlatform()
jvmArgs '--enable-native-access=ALL-UNNAMED'
systemProperty 'java.awt.headless', 'true'
systemProperty 'apple.awt.UIElement', 'true'
testLogging {
events "started", "failed"
showExceptions = true
showCauses = true
showStackTraces = true
exceptionFormat "full"
}
finalizedBy(jacocoReport)
}
jacocoReport.configure {
dependsOn(tasks.named("test"))
reports {
xml.required.set(true)
csv.required.set(false)
html.required.set(true)
}
doLast {
def xmlReport = reports.xml.outputLocation.get().asFile
if (!xmlReport.exists()) {
logger.lifecycle("Jacoco coverage report not found at ${xmlReport}")
return
}
def xmlContent = xmlReport.getText("UTF-8")
xmlContent = xmlContent.replaceFirst('(?s)<!DOCTYPE.*?>', '')
def report = new XmlSlurper(false, false).parseText(xmlContent)
def counters = report.counter.collectEntries { counter ->
def type = counter.@type.text()
def covered = counter.@covered.text() as BigDecimal
def missed = counter.@missed.text() as BigDecimal
[(type): [covered: covered, missed: missed]]
}
def thresholds = [
LINE : 0.13,
INSTRUCTION: 0.14,
BRANCH : 0.09
]
def types = ["LINE", "INSTRUCTION", "BRANCH"]
def headers = ["Metric", "Coverage", "Covered/Total", "Status", "Target"]
def rows = types.collect { String type ->
def data = counters[type]
if (!data) {
return [type, "—", "—", "No data", ""]
}
def total = data.covered + data.missed
if (total == 0) {
return [type, "—", "0/${total.toBigInteger()}", "No executions", ""]
}
def ratio = data.covered / total * 100
def coverageText = String.format(Locale.ROOT, "%.2f%%", ratio)
def coveredText = String.format(Locale.ROOT, "%d/%d",
data.covered.toBigInteger(),
total.toBigInteger())
def threshold = thresholds[type]
def thresholdPercent = threshold != null ? threshold * 100 : null
def targetText = thresholdPercent != null ?
String.format(Locale.ROOT, ">= %.2f%%", thresholdPercent) : ""
def passed = thresholdPercent != null ? ratio >= thresholdPercent : null
def statusText = passed == null ? "" : (passed ? "PASS" : "FAIL")
return [type, coverageText, coveredText, statusText, targetText]
}
def columnIndexes = (0..<headers.size())
def columnWidths = columnIndexes.collect { idx ->
Math.max(headers[idx].length(), rows.collect { row ->
row[idx] != null ? row[idx].toString().length() : 0
}.max() ?: 0)
}
def formatRow = { List<String> values ->
columnIndexes.collect { idx ->
def value = values[idx] ?: ""
value.padRight(columnWidths[idx])
}.join(" | ")
}
def separator = columnIndexes.collect { idx ->
''.padRight(columnWidths[idx], '-')
}.join("-+-")
logger.lifecycle("")
logger.lifecycle("==== JaCoCo Coverage Summary ====")
logger.lifecycle(formatRow(headers))
logger.lifecycle(separator)
rows.each { row ->
logger.lifecycle(formatRow(row))
}
logger.lifecycle(separator)
def htmlReport = reports.html.outputLocation.get().asFile
logger.lifecycle("Detailed HTML report available at: ${htmlReport}")
if (rows.any { it[3] == "FAIL" }) {
logger.lifecycle("Some coverage targets were missed. Please review the detailed report above.")
} else if (rows.any { it[3] == "PASS" }) {
logger.lifecycle("Great job! All tracked coverage metrics meet their targets.")
}
logger.lifecycle("=================================\n")
}
}
tasks.named("build") {
dependsOn jacocoReport
}
jacocoTestCoverageVerification {
dependsOn jacocoReport
violationRules {
rule {
enabled = true
element = 'BUNDLE'
// Bytecode-Anweisungen abgedeckt
limit {
counter = 'INSTRUCTION'
value = 'COVEREDRATIO'
minimum = 0.14
}
// wie viele Quellcode-Zeilen abgedeckt
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.13
}
// Verzweigungen (if/else, switch) abgedeckt; misst Logik-Abdeckung
limit {
counter = 'BRANCH'
value = 'COVEREDRATIO'
minimum = 0.09
}
}
}
}
tasks.named("processResources") {
dependsOn(rootProject.tasks.writeVersion)
}
if (name == 'stirling-pdf') {
apply plugin: 'org.springdoc.openapi-gradle-plugin'
openApi {
apiDocsUrl = "http://localhost:8080/v1/api-docs"
outputDir = file("$projectDir")
outputFileName = "SwaggerDoc.json"
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
}
tasks.named("forkedSpringBootRun") {
dependsOn(":common:jar")
dependsOn(":proprietary:jar")
}
tasks.register("copySwaggerDoc", Copy) {
doNotTrackState("Writes SwaggerDoc.json to project root")
from(layout.projectDirectory.file("SwaggerDoc.json"))
into(rootProject.projectDir)
dependsOn("generateOpenApiDocs")
}
tasks.register("cleanSwaggerInBuild", Delete) {
doNotTrackState("Cleans up SwaggerDoc.json in build directory")
delete(layout.projectDirectory.file("SwaggerDoc.json"))
dependsOn("copySwaggerDoc")
}
tasks.named("copySwaggerDoc") {
finalizedBy("cleanSwaggerInBuild")
}
tasks.named("generateOpenApiDocs") {
finalizedBy("copySwaggerDoc")
doNotTrackState("OpenAPI plugin writes outside build directory")
}
tasks.named("bootRun") {
def runtimeArgs = [
"-XX:+UseG1GC",
"-XX:MaxGCPauseMillis=200",
"-XX:G1HeapRegionSize=4m",
"-XX:+ExplicitGCInvokesConcurrent",
"-XX:+UseStringDeduplication",
"-XX:+UseCompactObjectHeaders",
"--enable-native-access=ALL-UNNAMED"
]
// Optional JaCoCo agent for e2e/cucumber backend coverage.
//
// Enabled with `-PjacocoAgent=true`. The default destfile lives
// outside the source tree so it doesn't poison a normal bootRun
// for a developer who forgot to pass the property. Override
// both with `-PjacocoExec=/path/to.exec` if you need a custom
// location (e.g. when sharing it with the Playwright runner).
if (rootProject.hasProperty('jacocoAgent') &&
rootProject.property('jacocoAgent').toString() == 'true') {
def agentJar = rootProject.layout.buildDirectory
.file('jacoco/jacocoagent.jar').get().asFile.absolutePath
def execFile = (rootProject.findProperty('jacocoExec') ?:
rootProject.layout.projectDirectory
.file('.test-state/playwright/jacoco.exec').asFile.absolutePath
).toString()
// The JaCoCo agent argument is a comma-separated key=value list,
// so a path that itself contains ',' or '=' would smuggle extra
// agent options (e.g. -PjacocoExec='out.exec,sessionid=evil').
// Today the value only comes from our own CI workflows, but
// validating defensively is cheap and silences Aikido.
//
// Control characters are checked via String.contains rather
// than inside a slashy regex character class - a literal NULL
// accidentally landing in the regex killed Groovy parsing on
// the previous attempt.
def hasBadChar = execFile.find(/[,= ]/) != null ||
execFile.contains('\r') ||
execFile.contains('\n') ||
execFile.contains('\t')
if (hasBadChar) {
throw new GradleException(
"jacocoExec='" + execFile + "' contains characters " +
"(',', '=', whitespace, or control chars) that " +
"would break -javaagent option parsing. Choose " +
"a different path."
)
}
runtimeArgs.add("-javaagent:${agentJar}=destfile=${execFile},output=file,append=false,dumponexit=true")
dependsOn(rootProject.tasks.named('copyJacocoAgent'))
doFirst {
new File(execFile).parentFile?.mkdirs()
logger.lifecycle("JaCoCo agent attached: ${execFile}")
}
}
jvmArgs = runtimeArgs
}
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
if (!project.hasProperty("noSpotless")) {
dependsOn "spotlessApply"
}
}
gradle.taskGraph.whenReady { graph ->
if (project.hasProperty("noSpotless")) {
allprojects { scopedProject ->
scopedProject.tasks.matching { it.name.startsWith("spotless") }.configureEach {
enabled = false
}
}
}
}
def allProjects = ((subprojects as Set<Project>) + project) as Set<Project>
def moduleLicenseOverridesFile = project.layout.projectDirectory.file("app/license-overrides.json").asFile
licenseReport {
projects = allProjects
renderers = [new JsonReportRenderer()]
allowedLicensesFile = project.layout.projectDirectory.file("app/allowed-licenses.json").asFile
outputDir = project.layout.buildDirectory.dir("reports/dependency-license").get().asFile.path
configurations = [ "productionRuntimeClasspath", "runtimeClasspath" ]
filters = [new ModuleLicenseOverrideFilter(moduleLicenseOverridesFile)]
}
tasks.named('generateLicenseReport') {
inputs.file(moduleLicenseOverridesFile)
}
tasks.named('checkLicensePreparation') {
inputs.file(moduleLicenseOverridesFile)
}
// Configure the forked spring boot run task to properly delegate to the stirling-pdf module
tasks.named('forkedSpringBootRun') {
dependsOn ':stirling-pdf:bootRun'
doFirst {
println "Delegating forkedSpringBootRun to :stirling-pdf:bootRun"
}
}
spotless {
yaml {
target '*.yml', '*.yaml'
trimTrailingWhitespace()
leadingTabsToSpaces()
endWithNewline()
}
format 'gradle', {
target 'build.gradle', 'settings.gradle', 'gradle/*.gradle', 'gradle/**/*.gradle'
trimTrailingWhitespace()
leadingTabsToSpaces()
endWithNewline()
}
}
sonar {
properties {
property "sonar.projectKey", "Stirling-Tools_Stirling-PDF"
property "sonar.organization", "stirling-tools"
property "sonar.exclusions", "**/build-wrapper-dump.json, **/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
property "sonar.coverage.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
property "sonar.cpd.exclusions", "**/src/main/java/org/apache/**, **/src/main/resources/static/pdfjs/**, **/src/main/resources/static/pdfjs-legacy/**, **/src/main/resources/static/js/thirdParty/**"
}
}
swaggerhubUpload {
// dependsOn = generateOpenApiDocs // Depends on your task generating Swagger docs
api = "Stirling-PDF" // The name of your API on SwaggerHub
owner = "${System.getenv().getOrDefault('SWAGGERHUB_USER', 'Frooodle')}" // Your SwaggerHub username (or organization name)
version = project.version // The version of your API
inputFile = file("SwaggerDoc.json") // The path to your Swagger docs
token = "${System.getenv("SWAGGERHUB_API_KEY")}" // Your SwaggerHub API key, passed as an environment variable
oas = "3.0.0" // The version of the OpenAPI Specification you"re using
}
repositories {
if (!rootProject.ext.mavenUrl.isEmpty()) {
maven {
url = rootProject.ext.mavenUrl + '/releases'
credentials(PasswordCredentials) {
username = rootProject.ext.username
password = rootProject.ext.password
}
authentication {
basic(BasicAuthentication)
}
allowInsecureProtocol = true
}
}
mavenCentral()
maven { url = "https://repository.jboss.org/" }
maven { url = "https://build.shibboleth.net/maven/releases" }
}
dependencies {
implementation project(':stirling-pdf')
implementation project(':common')
if (rootProject.ext.isSecurityDisabled()) {
implementation project(':proprietary')
}
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
testImplementation platform("com.squareup.okhttp3:okhttp-bom:${okhttpBomVersion}")
testImplementation "com.squareup.okhttp3:mockwebserver"
}
tasks.named("test") {
useJUnitPlatform()
}
// Make sure all relevant processes depend on writeVersion
processResources.dependsOn(writeVersion)
tasks.register('printVersion') {
doLast {
println project.version
}
}
tasks.named('bootRun') {
group = 'application'
description = 'Delegates to :stirling-pdf:bootRun'
dependsOn ':stirling-pdf:bootRun'
doFirst {
println "Delegating to :stirling-pdf:bootRun"
}
}
tasks.named('build') {
group = 'build'
description = 'Delegates to :stirling-pdf:bootJar'
dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper', 'syncAppVersion'
doFirst {
println "Delegating to :stirling-pdf:bootJar"
}
}
// Task to compile RestartHelper.java
tasks.register('compileRestartHelper', JavaCompile) {
group = 'build'
description = 'Compiles the RestartHelper utility'
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
classpath = files()
destinationDirectory = layout.buildDirectory.dir("restart-helper-classes")
def restartMajorVersion = buildJavaMajorVersion
def restartLanguageVersion = JavaLanguageVersion.of(restartMajorVersion)
def restartCompatibility = JavaVersion.toVersion(restartMajorVersion.toString())
sourceCompatibility = restartCompatibility
targetCompatibility = restartCompatibility
javaCompiler = javaToolchains.compilerFor {
languageVersion = restartLanguageVersion
}
options.release.set(restartMajorVersion)
}
// Task to create restart-helper.jar
tasks.register('buildRestartHelper', Jar) {
group = 'build'
description = 'Builds the restart-helper.jar'
dependsOn 'compileRestartHelper'
from layout.buildDirectory.dir("restart-helper-classes")
archiveFileName = 'restart-helper.jar'
destinationDirectory = layout.buildDirectory.dir("libs")
manifest {
attributes 'Main-Class': 'RestartHelper'
}
doLast {
println "restart-helper.jar created at: ${destinationDirectory.get()}/restart-helper.jar"
}
}
tasks.withType(Test).configureEach {
// maxParallelForks: parallelise JUnit across cores
// Half of available CPUs is a safe default; bump if your tests are I/O-bound.
maxParallelForks = Math.max(1, (Runtime.runtime.availableProcessors().intdiv(2)) as int)
}
// ----------------------------------------------------------------------------
// JaCoCo helpers used by CI for cucumber + Playwright (live backend) coverage.
//
// These let CI attach the JaCoCo agent to a running Spring Boot process
// (gradle bootRun or the docker image, via JAVA_TOOL_OPTIONS) and then turn
// the resulting .exec dump back into HTML + XML reports without anyone
// hand-installing the JaCoCo CLI.
// ----------------------------------------------------------------------------
configurations {
jacocoRuntimeAgent
jacocoCli
}
dependencies {
// The :runtime classifier on org.jacoco.agent IS the agent jar - no
// unzipping required. Kept on the version the plugin already picks so
// agent + reporter line up exactly.
jacocoRuntimeAgent "org.jacoco:org.jacoco.agent:${jacoco.toolVersion}:runtime"
jacocoCli "org.jacoco:org.jacoco.cli:${jacoco.toolVersion}"
}
tasks.register('copyJacocoAgent', Copy) {
group = 'verification'
description = 'Copies the JaCoCo runtime agent jar to build/jacoco/jacocoagent.jar.'
from configurations.jacocoRuntimeAgent
into layout.buildDirectory.dir('jacoco')
rename { 'jacocoagent.jar' }
}
// Aggregates an externally-produced .exec (e.g. from e2e:live or the cucumber
// docker container) against this project's compiled classes + sources.
//
// Inputs are configured via -P properties so the same task works for every
// caller:
//
// ./gradlew jacocoReportFromExec -PexecFile=.test-state/playwright/jacoco.exec \
// -PreportDir=build/reports/jacoco/e2e-live
tasks.register('jacocoReportFromExec', JacocoReport) {
group = 'verification'
description = 'Generates a JaCoCo HTML+XML report from an externally captured .exec.'
def execProp = project.findProperty('execFile') ?: project.findProperty('execFiles')
def reportProp = project.findProperty('reportDir') ?: "build/reports/jacoco/external"
executionData fileTree(rootProject.rootDir) {
if (execProp) {
include execProp.toString().split(',').collect { it.trim() }
} else {
// Sensible defaults so the task is usable without props.
include '.test-state/**/*.exec'
include 'coverage-tools/exec/*.exec'
include 'testing/cucumber-coverage/*.exec'
}
}
// Pull compiled classes + sources from every subproject so the report is
// an aggregate. JaCoCo silently skips classes that have no matching .exec
// probes, so this is safe even when only a subset of code was exercised.
classDirectories.setFrom(files(subprojects.collect { sub ->
sub.fileTree(dir: "${sub.buildDir}/classes/java/main", excludes: [
'**/generated/**',
])
}))
sourceDirectories.setFrom(files(subprojects.collect { sub ->
"${sub.projectDir}/src/main/java"
}))
reports {
xml.required.set(true)
html.required.set(true)
csv.required.set(false)
xml.outputLocation.set(layout.projectDirectory.file("${reportProp}/jacocoTestReport.xml"))
html.outputLocation.set(layout.projectDirectory.dir("${reportProp}/html"))
}
}