plugins { id 'java' id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' version '1.1.6' id 'io.openapiprocessor.openapi-processor' version '2023.2' id 'com.github.jk1.dependency-license-report' version '2.9' id "org.owasp.dependencycheck" version "10.0.4" id "com.diffplug.spotless" version "6.25.0" id 'jacoco' id 'info.solidsoft.pitest' version '1.15.0' id 'se.patrikerdes.use-latest-versions' version '0.2.18' id 'com.github.ben-manes.versions' version '0.51.0' } group = 'net.hostsharing' version = '0.0.1-SNAPSHOT' wrapper { distributionType = Wrapper.DistributionType.BIN gradleVersion = '8.5' } configurations { compileOnly { extendsFrom annotationProcessor } testCompile { extendsFrom testAnnotationProcessor // Only JUNit 5 (Jupiter) should be used at compile time. // For runtime it's still needed by testcontainers, though. exclude group: 'junit', module: 'junit' exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } } repositories { mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } } java { toolchain { languageVersion = JavaLanguageVersion.of(21) vendor = JvmVendorSpec.ADOPTIUM implementation = JvmImplementation.VENDOR_SPECIFIC } } ext { set('testcontainersVersion', "1.17.3") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-rest' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2' implementation 'org.springdoc:springdoc-openapi:2.6.0' implementation 'org.postgresql:postgresql:42.7.4' implementation 'org.liquibase:liquibase-core:4.29.2' implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'org.apache.commons:commons-text:1.12.0' implementation 'net.java.dev.jna:jna:5.15.0' implementation 'org.modelmapper:modelmapper:3.2.1' implementation 'org.iban4j:iban4j:3.2.10-RELEASE' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' implementation 'org.webjars:swagger-ui:5.17.14' implementation 'org.reflections:reflections:0.10.2' compileOnly 'org.projectlombok:lombok' testCompileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.testcontainers:testcontainers' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.testcontainers:postgresql' testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' testImplementation 'io.rest-assured:spring-mock-mvc' testImplementation 'org.hamcrest:hamcrest-core:3.0' testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1' testImplementation 'org.junit.jupiter:junit-jupiter-api' } dependencyManagement { imports { mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}" } } // Java Compiler Options tasks.withType(JavaCompile) { options.compilerArgs += [ "-parameters" // keep parameter names => no need for @Param for SpringData ] } // Configure tests tasks.named('test') { useJUnitPlatform() jvmArgs '-Duser.language=en' jvmArgs '-Duser.country=US' } // OpenAPI Source Code Generation openapiProcessor { springRoot { processorName 'spring' processor 'io.openapiprocessor:openapi-processor-spring:2022.5' apiPath "$projectDir/src/main/resources/api-definition/api-definition.yaml" mapping "$projectDir/src/main/resources/api-definition/api-mappings.yaml" targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true } springRbac { processorName 'spring' processor 'io.openapiprocessor:openapi-processor-spring:2022.5' apiPath "$projectDir/src/main/resources/api-definition/rbac/rbac.yaml" mapping "$projectDir/src/main/resources/api-definition/rbac/api-mappings.yaml" targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true } springTest { processorName 'spring' processor 'io.openapiprocessor:openapi-processor-spring:2022.5' apiPath "$projectDir/src/main/resources/api-definition/test/test.yaml" mapping "$projectDir/src/main/resources/api-definition/test/api-mappings.yaml" targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true } springHsOffice { processorName 'spring' processor 'io.openapiprocessor:openapi-processor-spring:2022.5' apiPath "$projectDir/src/main/resources/api-definition/hs-office/hs-office.yaml" mapping "$projectDir/src/main/resources/api-definition/hs-office/api-mappings.yaml" targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true } springHsBooking { processorName 'spring' processor 'io.openapiprocessor:openapi-processor-spring:2022.5' apiPath "$projectDir/src/main/resources/api-definition/hs-booking/hs-booking.yaml" mapping "$projectDir/src/main/resources/api-definition/hs-booking/api-mappings.yaml" targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true } springHsHosting { processorName 'spring' processor 'io.openapiprocessor:openapi-processor-spring:2022.5' apiPath "$projectDir/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml" mapping "$projectDir/src/main/resources/api-definition/hs-hosting/api-mappings.yaml" targetDir "$buildDir/generated/sources/openapi-javax" showWarnings true openApiNullable true } } sourceSets.main.java.srcDir 'build/generated/sources/openapi' abstract class ProcessSpring extends DefaultTask {} tasks.register('processSpring', ProcessSpring) ['processSpringRoot', 'processSpringRbac', 'processSpringTest', 'processSpringHsOffice', 'processSpringHsBooking', 'processSpringHsHosting' ].each { project.tasks.processSpring.dependsOn it } project.tasks.processResources.dependsOn processSpring project.tasks.compileJava.dependsOn processSpring // Rename javax to jakarta in OpenApi generated java files because // io.openapiprocessor.openapi-processor 2022.5 does not yet support the openapiprocessor useSpringBoot3 config option. // TODO.impl: Upgrade to io.openapiprocessor.openapi-processor >= 2024.2 // and use either `bean-validation: true` in api-mapping.yaml or `useSpringBoot3 true` (not sure where exactly). task openApiGenerate(type: Copy) { from "$buildDir/generated/sources/openapi-javax" into "$buildDir/generated/sources/openapi" filter { line -> line.replaceAll('javax', 'jakarta') } } compileJava.source "$buildDir/generated/sources/openapi" compileJava.dependsOn openApiGenerate openApiGenerate.dependsOn processSpring // Spotless Code Formatting spotless { java { removeUnusedImports() indentWithSpaces(4) endWithNewline() toggleOffOn() target fileTree(rootDir) { include '**/*.java' exclude '**/generated/**/*.java' } } } project.tasks.check.dependsOn(spotlessCheck) // HACK: no idea why spotless uses the output of these tasks, but we get warnings without those project.tasks.spotlessJava.dependsOn( tasks.generateLicenseReport, tasks.pitest, tasks.jacocoTestReport, tasks.processResources, tasks.processTestResources) // OWASP Dependency Security Test dependencyCheck { nvd { apiKey = project.properties['OWASP_API_KEY'] // set it in ~/.gradle/gradle.properties delay = 16000 } format = 'ALL' suppressionFile = 'etc/owasp-dependency-check-suppression.xml' failOnError = true failBuildOnCVSS = 5 } project.tasks.check.dependsOn(dependencyCheckAnalyze) project.tasks.dependencyCheckAnalyze.doFirst { // Why not doLast? See README.md! println "OWASP Dependency Security Report: file:///${project.rootDir}/build/reports/dependency-check-report.html" } // License Check licenseReport { excludeBoms = true allowedLicensesFile = new File("$projectDir/etc/allowed-licenses.json") } project.tasks.check.dependsOn(checkLicense) // JaCoCo Test Code Coverage jacoco { toolVersion = "0.8.10" } test { finalizedBy jacocoTestReport // generate report after tests excludes = [ 'net.hostsharing.hsadminng.**.generated.**', ] useJUnitPlatform { excludeTags 'importOfficeData', 'importHostingData', 'scenarioTest' } } jacocoTestReport { dependsOn test afterEvaluate { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: [ "net/hostsharing/hsadminng/**/generated/**/*.class", "net/hostsharing/hsadminng/hs/HsadminNgApplication.class" ]) })) } doFirst { // Why not doLast? See README.md! println "HTML Jacoco Test Code Coverage Report: file://${reports.html.outputLocation.get()}/index.html" } } project.tasks.check.dependsOn(jacocoTestCoverageVerification) jacocoTestCoverageVerification { violationRules { rule { limit { minimum = 0.80 // TODO.test: improve instruction coverage } } // element: PACKAGE, BUNDLE, CLASS, SOURCEFILE or METHOD // counter: INSTRUCTION, BRANCH, LINE, COMPLEXITY, METHOD, or CLASS // value: TOTALCOUNT, COVEREDCOUNT, MISSEDCOUNT, COVEREDRATIO or MISSEDRATIO rule { element = 'CLASS' excludes = [ 'net.hostsharing.hsadminng.**.generated.**', 'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity', 'net.hostsharing.hsadminng.HsadminNgApplication', 'net.hostsharing.hsadminng.ping.PingController', 'net.hostsharing.hsadminng.rbac.generator.*', 'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService', 'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node', 'net.hostsharing.hsadminng.**.*Repository', 'net.hostsharing.hsadminng.mapper.Mapper' ] limit { counter = 'LINE' value = 'COVEREDRATIO' minimum = 0.75 // TODO.test: improve line coverage } } rule { element = 'METHOD' excludes = [ 'net.hostsharing.hsadminng.**.generated.**', 'net.hostsharing.hsadminng.HsadminNgApplication.main', 'net.hostsharing.hsadminng.ping.PingController.*' ] limit { counter = 'BRANCH' value = 'COVEREDRATIO' minimum = 0.00 // TODO.test: improve branch coverage } } } } tasks.register('importOfficeData', Test) { useJUnitPlatform { includeTags 'importOfficeData' } group 'verification' description 'run the import jobs as tests' mustRunAfter spotlessJava } tasks.register('importHostingAssets', Test) { useJUnitPlatform { includeTags 'importHostingAssets' } group 'verification' description 'run the import jobs as tests' mustRunAfter spotlessJava } tasks.register('scenarioTests', Test) { useJUnitPlatform { includeTags 'scenarioTest' } group 'verification' description 'run the import jobs as tests' mustRunAfter spotlessJava } // pitest mutation testing pitest { targetClasses = ['net.hostsharing.hsadminng.**'] excludedClasses = [ 'net.hostsharing.hsadminng.config.**', // 'net.hostsharing.hsadminng.**.*Controller', 'net.hostsharing.hsadminng.**.generated.**' ] targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest'] excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*'] pitestVersion = '1.17.0' junit5PluginVersion = '1.1.0' threads = 4 // As Java unit tests are pretty pointless in our case, this maybe makes not much sense. mutationThreshold = 71 coverageThreshold = 57 testStrengthThreshold = 87 outputFormats = ['XML', 'HTML'] timestampedReports = false } project.tasks.check.dependsOn(project.tasks.pitest) project.tasks.pitest.doFirst { // Why not doLast? See README.md! println "PiTest Mutation Report: file:///${project.rootDir}/build/reports/pitest/index.html" } // Dependency Versions Upgrade useLatestVersions { finalizedBy check } def isNonStable = { String version -> def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.toUpperCase().contains(it) } def regex = /^[0-9,.v-]+(-r)?$/ return !stableKeyword && !(version ==~ regex) } tasks.named("dependencyUpdates").configure { rejectVersionIf { isNonStable(it.candidate.version) } } // Generate HTML from Markdown scenario-test-reports using Pandoc: tasks.register('convertMarkdownToHtml') { description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.' group = 'Conversion' // Define the template file and input directory def templateFile = file('doc/scenarios/.template.html') // Task configuration and execution doFirst { // Check if pandoc is installed try { exec { commandLine 'pandoc', '--version' } } catch (Exception) { throw new GradleException("Pandoc is not installed or not found in the system path.") } // Check if the template file exists if (!templateFile.exists()) { throw new GradleException("Template file 'doc/scenarios/.template.html' not found.") } } doLast { // Gather all Markdown files in the current directory fileTree(dir: '.', include: 'build/doc/scenarios/*.md').each { file -> // Corrected way to create the output file path def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html')) // Execute pandoc for each markdown file exec { commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath } println "Converted ${file.name} to ${outputFile.name}" } } } convertMarkdownToHtml.dependsOn scenarioTests