diff --git a/build.gradle b/build.gradle index 9054adf6a05..e6eb8dab56d 100644 --- a/build.gradle +++ b/build.gradle @@ -150,11 +150,39 @@ allprojects { mavenCentral() } + configurations { + api { + canBeResolved = true + } + } + apply plugin: 'com.adarshr.test-logger' apply plugin: 'com.diffplug.spotless' apply plugin: 'com.github.hierynomus.license' + apply plugin: 'net.ltgt.errorprone' + + tasks.withType(JavaCompile).configureEach { + options.errorprone { + disableWarningsInGeneratedCode = true + disable( + "CanIgnoreReturnValueSuggester", + "SameNameButDifferent", // Until errorprone recognizes Lombok + "MultiVariableDeclaration", // Until errorprone recognizes Lombok + "UnnecessaryDefaultInEnumSwitch", // FINERACT-1911 + "AssertEqualsArgumentOrderChecker", + "RemoveUnusedImports" // For generated code + ) + error( + "DefaultCharset", + "StringSplitter", + "MutablePublicArray", + "EqualsGetClass", + "FutureReturnValueIgnored" + ) + } + } + apply plugin: 'org.nosphere.apache.rat' - apply plugin: 'project-report' apply plugin: 'com.github.jk1.dependency-license-report' // Configuration for the sonarqube plugin is now in GitHub Actions @@ -324,10 +352,49 @@ configure(project.fineractJavaProjects) { apply plugin: 'java' apply plugin: 'idea' + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + withSourcesJar() + withJavadocJar() + } + + tasks.withType(ProcessResources) { + destinationDir = layout.buildDirectory.dir('classes/java/main').get().asFile + } + + // Add performance optimizations + configurations.all { + resolutionStrategy { + cacheChangingModulesFor 0, 'seconds' + cacheDynamicVersionsFor 0, 'seconds' + } + } + + tasks.withType(JavaCompile).configureEach { + options.incremental = true + options.fork = true + outputs.cacheIf { true } + options.compilerArgs << '-parameters' + options.encoding = 'UTF-8' + options.compilerArgs << '-Xlint:unchecked' + options.compilerArgs << '-Xlint:deprecation' + if (project.hasProperty('warnings') && project.warnings.contains('fail')) { + options.compilerArgs << '-Werror' + } + if (project.hasProperty('warnings') && project.warnings.contains('none')) { + options.compilerArgs << '-nowarn' + } + if (project.plugins.hasPlugin('org.springframework.boot')) { + options.generatedSourceOutputDirectory = file("$buildDir/generated/sources/annotationProcessor/java/main") + } + options.generatedSourceOutputDirectory = file("$buildDir/generated/sources/annotationProcessor/java/main") + } + apply plugin: 'eclipse' apply plugin: 'checkstyle' apply plugin: 'jacoco' - apply plugin: 'net.ltgt.errorprone' apply plugin: 'com.github.spotbugs' apply plugin: 'com.github.andygoossens.modernizer' apply from: "${rootDir}/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle" @@ -335,9 +402,9 @@ configure(project.fineractJavaProjects) { group = 'org.apache.fineract' /* define the valid syntax level for source files */ - sourceCompatibility = JavaVersion.VERSION_17 - /* define binary compatibility version */ - targetCompatibility = JavaVersion.VERSION_17 + // sourceCompatibility = JavaVersion.VERSION_17 + // /* define binary compatibility version */ + // targetCompatibility = JavaVersion.VERSION_17 /* http://stackoverflow.com/questions/19653311/jpa-repository-works-in-idea-and-production-but-not-in-gradle */ sourceSets.main.output.resourcesDir = sourceSets.main.java.classesDirectory @@ -353,7 +420,6 @@ configure(project.fineractJavaProjects) { } configurations { - implementation.setCanBeResolved(true) api.setCanBeResolved(true) } tasks.withType(Copy) { @@ -361,10 +427,8 @@ configure(project.fineractJavaProjects) { } tasks.withType(JavaCompile) { options.compilerArgs += [ - "-Xlint:unchecked", "-Xlint:cast", "-Xlint:auxiliaryclass", - "-Xlint:deprecation", "-Xlint:dep-ann", "-Xlint:divzero", "-Xlint:empty", @@ -398,6 +462,10 @@ configure(project.fineractJavaProjects) { options.deprecation = true } + check { + dependsOn(rat, licenseMain, licenseTest) + } + dependencies { spotbugsPlugins 'jp.skypencil.findbugs.slf4j:bug-pattern:1.5.0@jar' } @@ -469,7 +537,7 @@ configure(project.fineractJavaProjects) { reports { html.required = true xml.required = true - html.destination file("${buildDir}/code-coverage") + html.outputLocation = layout.buildDirectory.dir('code-coverage') } } @@ -479,11 +547,12 @@ configure(project.fineractJavaProjects) { errorprone "com.google.errorprone:error_prone_core:2.35.1" } - tasks.withType(JavaCompile) { + tasks.withType(JavaCompile).configureEach { options.errorprone { enabled = project.gradle.startParameter.taskNames.contains('build') || project.gradle.startParameter.taskNames.contains('check') - disableWarningsInGeneratedCode = true - excludedPaths = ".*/build/.*" + if (project.path == ':fineract-client') { + excludedPaths = '.*/build/generated/java/src/main/java/.*' + } disable( // TODO Remove disabled checks from this list, by fixing remaining usages "UnusedVariable", @@ -666,6 +735,19 @@ configure(project.fineractJavaProjects) { 'java/util/Optional.get:()Ljava/lang/Object;' // Disable forcing the usage of Optional.orElseThrow(java.util.function.Supplier) ] } + + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + options.encoding = 'UTF-8' + // Disable strict checking to prevent build failures on invalid javadoc + options.addBooleanOption('html5', true) + // Add this if you're using Java 17 records or other modern features + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { + options.addBooleanOption('html5', true) + } + // Ignore any errors during javadoc generation + failOnError = false + } } configure(project.fineractCustomProjects) { diff --git a/custom/acme/event/externalevent/build.gradle b/custom/acme/event/externalevent/build.gradle index bb703fb8a34..57ad8a7a4b7 100644 --- a/custom/acme/event/externalevent/build.gradle +++ b/custom/acme/event/externalevent/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract external events' +description = 'ACME Fineract Event External Event' -group = 'com.acme.fineract.event' +group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-event-externalevent' +base { + archivesName = 'acme-fineract-event-externalevent' +} apply from: 'dependencies.gradle' diff --git a/custom/acme/event/starter/build.gradle b/custom/acme/event/starter/build.gradle index 314a45cf4a2..a8981a976b8 100644 --- a/custom/acme/event/starter/build.gradle +++ b/custom/acme/event/starter/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract Event Starter' +description = 'ACME Fineract Event Starter' -group = 'com.acme.fineract.event' +group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-event-starter' +base { + archivesName = 'acme-fineract-event-starter' +} apply from: 'dependencies.gradle' diff --git a/custom/acme/loan/cob/build.gradle b/custom/acme/loan/cob/build.gradle index 04ca760dcc3..a9ab5088844 100644 --- a/custom/acme/loan/cob/build.gradle +++ b/custom/acme/loan/cob/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract COB Loan' +description = 'ACME Fineract Loan COB' group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-loan-cob' +base { + archivesName = 'acme-fineract-loan-cob' +} apply from: 'dependencies.gradle' diff --git a/custom/acme/loan/job/build.gradle b/custom/acme/loan/job/build.gradle index 235711ad777..d021b213aa9 100644 --- a/custom/acme/loan/job/build.gradle +++ b/custom/acme/loan/job/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract Loan Job' +description = 'ACME Fineract Loan Job' group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-loan-job' +base { + archivesName = 'acme-fineract-loan-job' +} apply from: 'dependencies.gradle' diff --git a/custom/acme/loan/processor/build.gradle b/custom/acme/loan/processor/build.gradle index c693255c9aa..6ff821a31b3 100644 --- a/custom/acme/loan/processor/build.gradle +++ b/custom/acme/loan/processor/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract Loan Transaction Processors' +description = 'ACME Fineract Loan Processor' group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-loan-processor' +base { + archivesName = 'acme-fineract-loan-processor' +} apply from: 'dependencies.gradle' diff --git a/custom/acme/loan/starter/build.gradle b/custom/acme/loan/starter/build.gradle index 8620f5bfdc3..41e4b85ebfc 100644 --- a/custom/acme/loan/starter/build.gradle +++ b/custom/acme/loan/starter/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract COB Starter' +description = 'ACME Fineract Loan Starter' group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-loan-starter' +base { + archivesName = 'acme-fineract-loan-starter' +} apply from: 'dependencies.gradle' diff --git a/custom/acme/note/service/build.gradle b/custom/acme/note/service/build.gradle index 90fdb314feb..2c3adebbbee 100644 --- a/custom/acme/note/service/build.gradle +++ b/custom/acme/note/service/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract Note Service' +description = 'ACME Fineract Note Service' -group = 'com.acme.fineract.portfolio.note' +group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-note-service' +base { + archivesName = 'acme-fineract-note-service' +} apply from: 'dependencies.gradle' diff --git a/custom/acme/note/starter/build.gradle b/custom/acme/note/starter/build.gradle index 49bf235b51d..2fb60579859 100644 --- a/custom/acme/note/starter/build.gradle +++ b/custom/acme/note/starter/build.gradle @@ -16,10 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -description = 'ACME Corp.: Fineract Note Starter' +description = 'ACME Fineract Note Starter' -group = 'com.acme.fineract.portfolio.note' +group = 'com.acme.fineract' -archivesBaseName = 'acme-fineract-note-starter' +base { + archivesName = 'acme-fineract-note-starter' +} apply from: 'dependencies.gradle' diff --git a/fineract-accounting/build.gradle b/fineract-accounting/build.gradle index 3018795aaca..916069b1f94 100644 --- a/fineract-accounting/build.gradle +++ b/fineract-accounting/build.gradle @@ -31,7 +31,7 @@ compileJava.doLast { javaexec { description = 'Performs EclipseLink static weaving of entity classes' def target = source - main 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave' + mainClass = 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave' args '-persistenceinfo', source, source, target classpath sourceSets.main.runtimeClasspath } diff --git a/fineract-avro-schemas/build.gradle b/fineract-avro-schemas/build.gradle index ea2051dd38a..905c02c283d 100644 --- a/fineract-avro-schemas/build.gradle +++ b/fineract-avro-schemas/build.gradle @@ -26,18 +26,39 @@ apply plugin: 'com.github.davidmc24.gradle.plugin.avro-base' apply from: 'dependencies.gradle' -task preprocessAvroSchemas() { - doLast { - copy { - from "$projectDir/src/main/avro" - into "$buildDir/generated/avro/src/main/avro" - filter { line -> - line.replaceAll("\"bigdecimal\"", new File("$projectDir/src/main/resources/avro-templates/bigdecimal.avsc").getText("UTF-8")) +abstract class PreprocessAvroSchemasTask extends DefaultTask { + @InputDirectory + abstract DirectoryProperty getInputDir() + + @InputFile + abstract RegularFileProperty getBigDecimalTemplate() + + @OutputDirectory + abstract DirectoryProperty getOutputDir() + + @TaskAction + def preprocess() { + def template = getBigDecimalTemplate().get().asFile.getText("UTF-8") + def input = getInputDir().get().asFile + def output = getOutputDir().get().asFile + + input.eachFileRecurse { file -> + if (file.isFile()) { + def relativePath = input.toPath().relativize(file.toPath()) + def targetFile = output.toPath().resolve(relativePath).toFile() + targetFile.parentFile.mkdirs() + targetFile.text = file.text.replaceAll("\"bigdecimal\"", template) } } } } +tasks.register('preprocessAvroSchemas', PreprocessAvroSchemasTask) { + inputDir = file("$projectDir/src/main/avro") + bigDecimalTemplate = file("$projectDir/src/main/resources/avro-templates/bigdecimal.avsc") + outputDir = file("$buildDir/generated/avro/src/main/avro") +} + task buildJavaSdk(type: GenerateAvroJavaTask) { source("$buildDir/generated/avro/src/main/avro") outputDir = file("$buildDir/generated/java/src/main/java") @@ -61,7 +82,10 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { + options.errorprone { + enabled = false // Disable ErrorProne for this module since it contains generated code + } options.compilerArgs -= ["-Werror"] } @@ -78,3 +102,7 @@ sourceSets.main.java.srcDir new File(buildDir, "generated/java/src/main/java") licenseFormatMain.dependsOn buildJavaSdk licenseMain.dependsOn licenseFormatMain + +tasks.named('sourcesJar') { + dependsOn tasks.named('buildJavaSdk') +} diff --git a/fineract-client/build.gradle b/fineract-client/build.gradle index 485493e1829..a079f0a0866 100644 --- a/fineract-client/build.gradle +++ b/fineract-client/build.gradle @@ -107,29 +107,83 @@ task buildAsciidoc(type: org.openapitools.generator.gradle.plugin.tasks.Generate dependsOn(':fineract-provider:resolve') } +// Configure source sets with proper output directories +sourceSets { + main { + java { + srcDir new File(buildDir, "generated/java/src/main/java") + destinationDirectory = layout.buildDirectory.dir('classes/java/main').get().asFile + } + output.resourcesDir = layout.buildDirectory.dir('resources/main').get().asFile + } + test { + java { + destinationDirectory = layout.buildDirectory.dir('classes/java/test').get().asFile + } + output.resourcesDir = layout.buildDirectory.dir('resources/test').get().asFile + } +} + +// Configure jar tasks to handle duplicates +tasks.withType(Jar).configureEach { + // Handle duplicates by using the first occurrence + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +// Improve the cleanup task to track inputs and outputs task cleanupGeneratedJavaFiles() { + def tempDir = file("$buildDir/generated/temp-java") + def targetDir = file("$buildDir/generated/java") + + inputs.dir(tempDir) + outputs.dir(targetDir) + doLast { copy { - from "$buildDir/generated/temp-java".toString() - into "$buildDir/generated/java".toString() + from tempDir + into targetDir filter { line -> line - // This is a temporary step to get rid of joda imports in the generated code - // At this point it's unknown why it's even generated, probably it's a bug in the generator - .replaceAll("import org\\.joda\\.time\\.\\*;", "") - // The 3 lines below handles the cases when a request body is not required - .replaceAll(", \\)", ")") - .replaceAll(", , @HeaderMap", ", @HeaderMap") - .replaceAll("\\(, ", "(") + .replaceAll("import org\\.joda\\.time\\.\\*;", "") + .replaceAll(", \\)", ")") + .replaceAll(", , @HeaderMap", ", @HeaderMap") + .replaceAll("\\(, ", "(") } + // Also set duplicates strategy for the copy task + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } } - dependsOn("buildJavaSdk") } -// TODO: @vidakovic we could provide even more client libs in different languages (Go, Ruby, Swift etc.) -compileJava.dependsOn(buildJavaSdk, buildTypescriptAngularSdk, buildAsciidoc, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply) +// Configure Java compilation +tasks.named('compileJava') { + outputs.cacheIf { true } + dependsOn(buildJavaSdk, buildTypescriptAngularSdk, buildAsciidoc, cleanupGeneratedJavaFiles, licenseFormatMain, spotlessMiscApply) + mustRunAfter(licenseFormatMain, cleanupGeneratedJavaFiles) +} + +// Configure sources jar task +tasks.named('sourcesJar') { + dependsOn(cleanupGeneratedJavaFiles) + mustRunAfter(cleanupGeneratedJavaFiles) + + from(sourceSets.main.java.srcDirs) { + include "**/*.java" + } +} + +// Configure license formatting +tasks.named('licenseFormatMain') { + dependsOn(cleanupGeneratedJavaFiles) + mustRunAfter(cleanupGeneratedJavaFiles) + source = sourceSets.main.java.srcDirs +} + +tasks.named('licenseMain') { + dependsOn(licenseFormatMain) + mustRunAfter(licenseFormatMain) +} java { // keep this at Java 8, not 17; see https://issues.apache.org/jira/browse/FINERACT-1214 @@ -137,26 +191,17 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -tasks.withType(JavaCompile) { - // the generated code in build/generated/java/src/main/java/org/apache/fineract/client/auth/OAuthOkHttpClient.java#L71 currently uses deprecated RequestBody.create(MediaType,String) - // TODO FINERACT-1247 why does this not work: - // options.compilerArgs -= ["-Xlint:deprecation"] - // options.compilerArgs += ["-Xlint:-deprecation"] - // So we just have to use: - options.compilerArgs -= ["-Werror"] -} - -configurations { - generatedCompileClasspath.extendsFrom implementation - generatedRuntimeClasspath.extendsFrom runtimeClasspath +tasks.withType(JavaCompile).configureEach { + options.errorprone { + excludedPaths = '.*/build/generated/java/src/main/java/.*' + } } test { useJUnitPlatform() } -sourceSets.main.java.srcDir new File(buildDir, "generated/java/src/main/java") - -// NOTE: Gradle suggested these dependencies -licenseFormatMain.dependsOn buildJavaSdk -licenseMain.dependsOn licenseFormatMain +configurations { + generatedCompileClasspath.extendsFrom implementation + generatedRuntimeClasspath.extendsFrom runtimeClasspath +} diff --git a/fineract-core/build.gradle b/fineract-core/build.gradle index f02abf9a603..a914a886c4a 100644 --- a/fineract-core/build.gradle +++ b/fineract-core/build.gradle @@ -31,7 +31,7 @@ compileJava.doLast { javaexec { description = 'Performs EclipseLink static weaving of entity classes' def target = source - main 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave' + mainClass = 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave' args '-persistenceinfo', source, '-classpath', sourceSets.main.runtimeClasspath, source, target classpath sourceSets.main.runtimeClasspath } diff --git a/fineract-doc/build.gradle b/fineract-doc/build.gradle index 4338bc29bd2..0504c6ed879 100644 --- a/fineract-doc/build.gradle +++ b/fineract-doc/build.gradle @@ -25,8 +25,8 @@ asciidoctorj { attributes = [ version: "${project.version}", generated: "${buildDir}/generated/asciidoc", - imagesdir: "${projectDir}/src/docs/en/images", - diagramsdir: "${projectDir}/src/docs/en/diagrams", + imagesdir: "${buildDir}/generated/images", + diagramsdir: "${buildDir}/generated/diagrams", years: '2015-2024', revnumber: "${project.version}".toString(), rootdir: "${rootDir}".toString(), @@ -59,6 +59,16 @@ asciidoctor { dependsOn(':fineract-client:clean', ':fineract-client:buildAsciidoc') } +task copyImages(type: Copy) { + from "${projectDir}/src/docs/en/images" + into "${buildDir}/generated/images" +} + +task copyDiagrams(type: Copy) { + from "${projectDir}/src/docs/en/diagrams" + into "${buildDir}/generated/diagrams" +} + asciidoctorPdf { languages 'en' @@ -71,6 +81,8 @@ asciidoctorPdf { logging.captureStandardError LogLevel.INFO + dependsOn copyImages, copyDiagrams + // TODO: @vidakovic prepare a nicer theme // theme 'fineract-default' // pdfThemes { diff --git a/fineract-e2e-tests-core/build.gradle b/fineract-e2e-tests-core/build.gradle index 14ccb5ff380..b5d1e35bde9 100644 --- a/fineract-e2e-tests-core/build.gradle +++ b/fineract-e2e-tests-core/build.gradle @@ -21,10 +21,41 @@ plugins { id 'java' } +// Configure source sets with proper output directories +sourceSets { + test { + java { + destinationDirectory = layout.buildDirectory.dir('classes/java/test').get().asFile + } + resources { + destinationDirectory = layout.buildDirectory.dir('resources/test').get().asFile + } + } +} + repositories { mavenCentral() } +// Configure test compilation +tasks.named('compileTestJava') { + description = 'Compiles test Java source files' + + // Enable caching + outputs.cacheIf { true } + + // Configure compiler options + options.compilerArgs.add("-parameters") + + // Ensure proper output tracking + outputs.dir(sourceSets.test.java.destinationDirectory) + .withPropertyName("testClassesDir") + + // Track annotation processor outputs + options.annotationProcessorGeneratedSourcesDirectory = + layout.buildDirectory.dir('generated/sources/annotationProcessor/java/test').get().asFile +} + dependencies { testImplementation(project(':fineract-avro-schemas')) testImplementation(project(':fineract-client')) diff --git a/fineract-e2e-tests-runner/build.gradle b/fineract-e2e-tests-runner/build.gradle index d8da800854e..29745a4a18d 100644 --- a/fineract-e2e-tests-runner/build.gradle +++ b/fineract-e2e-tests-runner/build.gradle @@ -78,14 +78,29 @@ tasks.named('cucumber').get().dependsOn 'spotlessCheck' cucumber { tags = 'not @ignore' - main = 'io.cucumber.core.cli.Main' shorten = 'argfile' plugin = [ 'pretty', - 'io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm' + 'html:build/reports/cucumber/report.html', + 'json:build/reports/cucumber/report.json' ] } +tasks.cucumber { + doLast { + javaexec { + mainClass = 'io.cucumber.core.cli.Main' + classpath = configurations.cucumber + sourceSets.main.output + sourceSets.test.output + args = [ + '--plugin', 'pretty', + '--plugin', "html:${buildDir}/reports/cucumber/report.html", + '--plugin', "json:${buildDir}/reports/cucumber/report.json", + '--tags', 'not @ignore' + ] + } + } +} + allure { version = '2.17.3' } diff --git a/fineract-provider/build.gradle b/fineract-provider/build.gradle index 8f4f2121eb2..1e29a5d0ad4 100644 --- a/fineract-provider/build.gradle +++ b/fineract-provider/build.gradle @@ -30,25 +30,125 @@ apply plugin: 'se.thinkcode.cucumber-runner' check.dependsOn('cucumber') -compileJava.doLast { - def mainSS = sourceSets.main - def source = mainSS.java.classesDirectory.get() - copy { - from file("src/main/resources/jpa/persistence.xml") - into "${source}/META-INF/" +// Define a separate task for static weaving +def staticWeaveTask = tasks.register('staticWeave') { + description = 'Performs EclipseLink static weaving of entity classes' + + def weaveDir = layout.buildDirectory.dir('classes/java/weaved').get().asFile + def classesDir = layout.buildDirectory.dir('classes/java/main').get().asFile + def testClassesDir = layout.buildDirectory.dir('classes/java/test').get().asFile + def persistenceFile = file("src/main/resources/jpa/persistence.xml") + + inputs.files(sourceSets.main.java.classesDirectory, sourceSets.test.java.classesDirectory) + .withPropertyName("classFiles") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.file(persistenceFile) + .withPropertyName("persistenceXml") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.property("javaVersion", System.getProperty("java.version")) + + outputs.dir(weaveDir) + .withPropertyName("weavedClasses") + + // Enable caching for this task + outputs.cacheIf { true } + + // Use mustRunAfter instead of dependsOn to avoid circular dependencies + mustRunAfter 'compileJava', 'compileTestJava', 'processResources', 'processTestResources', 'generateGitProperties', 'resolve' + + doLast { + // Create weave directory + weaveDir.mkdirs() + + // Copy all class files to weave directory + copy { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from classesDir + from testClassesDir + into weaveDir + } + + // Copy persistence.xml + copy { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from persistenceFile + into "${weaveDir}/META-INF/" + } + + // Perform static weaving + javaexec { + description = 'Performs EclipseLink static weaving of entity classes' + mainClass = 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave' + args '-persistenceinfo', weaveDir, weaveDir, weaveDir + classpath sourceSets.main.runtimeClasspath + sourceSets.test.runtimeClasspath + } + + // Clean up persistence.xml + delete("${weaveDir}/META-INF/persistence.xml") + + // Copy weaved classes back to respective directories + copy { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from weaveDir + into classesDir + include '**/*.class' + exclude 'org/apache/fineract/**/test/**' + } + + copy { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from weaveDir + into testClassesDir + include 'org/apache/fineract/**/test/**/*.class' + } } - javaexec { - description = 'Performs EclipseLink static weaving of entity classes' - def target = source - main 'org.eclipse.persistence.tools.weaving.jpa.StaticWeave' - args '-persistenceinfo', source, source, target - classpath sourceSets.main.runtimeClasspath +} + +// Configure classes task +tasks.named('classes') { + finalizedBy staticWeave +} + +// Configure test classes task +tasks.named('testClasses') { + finalizedBy staticWeave +} + +// Make sure resolve task runs before static weaving +tasks.named('staticWeave') { + dependsOn 'resolve' +} + +// Configure proper output directories for main and test +sourceSets { + main { + java { + destinationDirectory = layout.buildDirectory.dir('classes/java/main').get().asFile + } + resources { + destinationDirectory = layout.buildDirectory.dir('resources/main').get().asFile + } } - delete { - delete "${source}/META-INF/persistence.xml" + test { + java { + destinationDirectory = layout.buildDirectory.dir('classes/java/test').get().asFile + } + resources { + destinationDirectory = layout.buildDirectory.dir('resources/test').get().asFile + } } } +// Configure the compile task +tasks.named('compileJava') { + // Ensure proper output tracking + outputs.cacheIf { true } + outputs.dir(sourceSets.main.java.destinationDirectory) + + // Remove any overlapping outputs configuration + outputs.doNotCacheIf("Has no overlapping outputs") { false } +} + // Configuration for Swagger documentation generation task // https://github.com/swagger-api/swagger-core/tree/master/modules/swagger-gradle-plugin import org.apache.tools.ant.filters.ReplaceTokens @@ -82,6 +182,11 @@ resolve { configurations { providedRuntime // needed for Spring Boot executable WAR providedCompile + implementation + testImplementation + cucumberRuntime { + extendsFrom testImplementation + } compile() { exclude module: 'hibernate-entitymanager' exclude module: 'hibernate-validator' @@ -100,6 +205,16 @@ configurations { runtime } +dependencies { + implementation project(':fineract-core') + implementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.mockito:mockito-core' + implementation 'org.mockito:mockito-junit-jupiter' + implementation 'org.junit.jupiter:junit-jupiter-api' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.liquibase:liquibase-core' +} + apply from: 'dependencies.gradle' // Configuration for the modernizer plugin @@ -318,17 +433,28 @@ task migrateDatabase { cucumber { tags = 'not @ignore' - main = 'io.cucumber.core.cli.Main' shorten = 'argfile' plugin = [ 'pretty', 'html:build/reports/cucumber/report.html', - 'json:build/reports/cucumber/report.json', - 'junit:build/reports/cucumber/report.xml' + 'json:build/reports/cucumber/report.json' ] } -tasks.jibDockerBuild.dependsOn(bootJar) +tasks.cucumber { + doLast { + javaexec { + mainClass = 'io.cucumber.core.cli.Main' + classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output + args = [ + '--plugin', 'pretty', + '--plugin', "html:${buildDir}/reports/cucumber/report.html", + '--plugin', "json:${buildDir}/reports/cucumber/report.json", + '--tags', 'not @ignore' + ] + } + } +} // Configuration for git properties gradle plugin // https://github.com/n0mer/gradle-git-properties @@ -352,3 +478,40 @@ spotbugsTest.dependsOn resolve compileTestJava.dependsOn ':fineract-client:processResources', ':fineract-avro-schemas:processResources' resolveMainClassName.dependsOn resolve processResources.dependsOn compileJava + +javadoc { + dependsOn resolve +} + +task devRun(type: org.springframework.boot.gradle.tasks.run.BootRun) { + description = 'Runs the application quickly for development by skipping quality checks' + group = 'Application' + + // Configure the build to skip quality checks + gradle.taskGraph.whenReady { graph -> + if (graph.hasTask(devRun)) { + tasks.matching { task -> + task.name in ['checkstyle', 'checkstyleMain', 'checkstyleTest', + 'spotlessCheck', 'spotlessApply', + 'spotbugsMain', 'spotbugsTest', + 'javadoc', 'javadocJar', + 'modernizer'] + }.configureEach { + enabled = false + } + // Also disable error prone compilation flags + tasks.withType(JavaCompile).configureEach { + options.errorprone.enabled = false + } + } + } + + // Inherit all bootRun settings + classpath = bootRun.classpath + mainClass = bootRun.mainClass + jvmArgs = bootRun.jvmArgs + + doFirst { + println "Running in development mode - quality checks are disabled" + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java b/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java index aa6a210aa1b..5002fae691a 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java +++ b/fineract-provider/src/test/java/org/apache/fineract/TestConfiguration.java @@ -42,7 +42,6 @@ import org.apache.fineract.infrastructure.jobs.ScheduledJobRunnerConfig; import org.apache.fineract.infrastructure.jobs.service.JobRegisterService; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -103,7 +102,7 @@ public HikariDataSource create(FineractPlatformTenant tenant) { @Primary @Bean public HikariDataSource tenantDataSource() { - HikariDataSource mockDataSource = mock(HikariDataSource.class, Mockito.RETURNS_MOCKS); + HikariDataSource mockDataSource = mock(HikariDataSource.class, RETURNS_MOCKS); return mockDataSource; } @@ -112,7 +111,7 @@ public HikariDataSource tenantDataSource() { */ @Bean public RoutingDataSource hikariTenantDataSource() { - RoutingDataSource mockDataSource = mock(RoutingDataSource.class, Mockito.RETURNS_MOCKS); + RoutingDataSource mockDataSource = mock(RoutingDataSource.class, RETURNS_MOCKS); return mockDataSource; } @@ -128,17 +127,17 @@ public DatabaseTypeResolver databaseTypeResolver() { @Primary @Bean public TenantDetailsService tenantDetailsService() { - return mock(TenantDetailsService.class, Mockito.RETURNS_MOCKS); + return mock(TenantDetailsService.class, RETURNS_MOCKS); } @Bean public ExtendedSpringLiquibaseFactory liquibaseFactory() { - return mock(ExtendedSpringLiquibaseFactory.class, Mockito.RETURNS_MOCKS); + return mock(ExtendedSpringLiquibaseFactory.class, RETURNS_MOCKS); } @Bean public DatabaseIndependentQueryService databaseIndependentQueryService() { - return mock(DatabaseIndependentQueryService.class, Mockito.RETURNS_MOCKS); + return mock(DatabaseIndependentQueryService.class, RETURNS_MOCKS); } @Bean diff --git a/fineract-war/build.gradle b/fineract-war/build.gradle index 41c79e0ffe6..c7a29aad0e4 100644 --- a/fineract-war/build.gradle +++ b/fineract-war/build.gradle @@ -23,28 +23,58 @@ apply plugin: 'distribution' apply from: "${rootDir}/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle" war { + description = 'Assembles a WAR archive containing the web application' archiveFileName = 'fineract-provider.war' - from("$rootDir/licenses/binary/") { - // notice the parens - into "WEB-INF/licenses/binary/" // no leading slash + + // Enable caching + outputs.cacheIf { true } + + // Track inputs explicitly + inputs.files(project.configurations.runtimeClasspath) + .withPropertyName("runtimeClasspath") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.files(project.sourceSets.main.output.classesDirs) + .withPropertyName("classes") + .withPathSensitivity(PathSensitivity.RELATIVE) + + def licensesDir = file("$rootDir/licenses/binary/") + if (licensesDir.exists()) { + inputs.dir(licensesDir) + .withPropertyName("licenses") + .withPathSensitivity(PathSensitivity.RELATIVE) + + from(licensesDir) { + into "WEB-INF/licenses/binary/" + } } - from("$rootDir/LICENSE_RELEASE") { - // notice the parens - into "WEB-INF/" // no leading slash + + def legalFiles = [ + file("$rootDir/LICENSE_RELEASE"), + file("$rootDir/NOTICE_RELEASE"), + file("$rootDir/DISCLAIMER") + ].findAll { it.exists() } + + if (!legalFiles.empty) { + inputs.files(legalFiles) + .withPropertyName("legalFiles") + .withPathSensitivity(PathSensitivity.RELATIVE) } - from("$rootDir/NOTICE_RELEASE") { - // notice the parens - into "WEB-INF/" // no leading slash + + legalFiles.each { file -> + from(file) { + into "WEB-INF/" + } } + rename ('LICENSE_RELEASE', 'LICENSE') rename ('NOTICE_RELEASE', 'NOTICE') - - from("$rootDir/DISCLAIMER") { - // notice the parens - into "WEB-INF/" // no leading slash - } + enabled = true archiveClassifier = '' + + // Ensure reproducible output + preserveFileTimestamps = false + reproducibleFileOrder = true } dependencies { @@ -64,12 +94,24 @@ dependencies { tasks.withType(Tar) { compression Compression.GZIP archiveExtension = 'tar.gz' + + // Enable caching for all tar tasks + outputs.cacheIf { true } + + // Ensure reproducible output + preserveFileTimestamps = false + reproducibleFileOrder = true } distributions { binary { distributionBaseName = 'apache-fineract-binary' contents { + // Track inputs explicitly for binary distribution + filesMatching('**/*.jar') { + it.path = it.path.replaceAll('-\\d+\\.\\d+\\.\\d+(-SNAPSHOT)?', '-' + version) + } + from ("$rootDir/fineract-client/build/libs/") { include 'fineract-client-*.jar' } @@ -99,19 +141,52 @@ distributions { src { distributionBaseName = 'apache-fineract-src' contents { - from "$rootDir/" - exclude '**/build' , '.git', '**/.gradle', '.github', '**/.settings', '**/.project', '**/.classpath', '.idea', 'out', '._.DS_Store', '.DS_Store', 'WebContent', '**/.externalToolbuilders', '.theia', '.gitpod.yml', 'LICENSE_RELEASE', 'NOTICE_RELEASE', '**/licenses', '*.class', '**/bin', '*.log', '.dockerignore', '**/.gitkeep' + // Track inputs explicitly for source distribution + from("$rootDir/") { + exclude '**/build' , '.git', '**/.gradle', '.github', '**/.settings', '**/.project', + '**/.classpath', '.idea', 'out', '._.DS_Store', '.DS_Store', 'WebContent', + '**/.externalToolbuilders', '.theia', '.gitpod.yml', 'LICENSE_RELEASE', + 'NOTICE_RELEASE', '**/licenses', '*.class', '**/bin', '*.log', '.dockerignore', + '**/.gitkeep' + + // Ensure consistent file paths for caching + eachFile { details -> + details.path = details.path.replace('\\', '/') + } + } rename ('LICENSE_SOURCE', 'LICENSE') rename ('NOTICE_SOURCE', 'NOTICE') } } +} + +// Configure specific tar tasks +tasks.named('binaryDistTar') { + description = 'Assembles the binary distribution as a tar archive' + outputs.cacheIf { true } + + // Track dependencies explicitly + dependsOn(war, ':fineract-client:jar', ':fineract-avro-schemas:jar', + ':fineract-provider:build', ':fineract-doc:doc', + ':fineract-client:javadocJar', ':fineract-client:sourcesJar', + ':fineract-avro-schemas:javadocJar', ':fineract-avro-schemas:sourcesJar') + + doLast { + file("${buildDir}/distributions/apache-fineract-binary-${version}.tar.gz") + .renameTo("${buildDir}/distributions/apache-fineract-${version}-binary.tar.gz") + } +} + +tasks.named('srcDistTar') { + description = 'Assembles the source distribution as a tar archive' + outputs.cacheIf { true } + doLast { - file("${buildDir}/distributions/apache-fineract-binary-${version}.tar.gz").renameTo("${buildDir}/distributions/apache-fineract-${version}-binary.tar.gz") - file("${buildDir}/distributions/apache-fineract-src-${version}.tar.gz").renameTo("${buildDir}/distributions/apache-fineract-${version}-src.tar.gz") + file("${buildDir}/distributions/apache-fineract-src-${version}.tar.gz") + .renameTo("${buildDir}/distributions/apache-fineract-${version}-src.tar.gz") } } +// Disable zip distributions as they're not needed binaryDistZip.enabled false srcDistZip.enabled false -// NOTE: Gradle suggested these dependencies -binaryDistTar.dependsOn(war, ':fineract-client:jar', ':fineract-avro-schemas:jar', ':fineract-provider:build', ':fineract-doc:doc') diff --git a/gradle.properties b/gradle.properties index d2b7f204fff..022d5cbcec0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,3 +20,7 @@ org.gradle.jvmargs=-Xmx6g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL buildType=BUILD org.gradle.caching=true org.gradle.parallel=true +org.gradle.daemon.idletimeout=10800000 +# Temporarily disabled until we fix configuration cache compatibility +#org.gradle.configuration-cache=true +org.gradle.vfs.watch=true diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index c5839bf3521..7ef84a2c95a 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -97,6 +97,18 @@ if (!project.hasProperty('cargoDisabled')) { } } +// Configure proper test output directories +sourceSets { + test { + output.resourcesDir = layout.buildDirectory.dir('resources/test').get().asFile + java.destinationDirectory = layout.buildDirectory.dir('classes/java/test').get().asFile + } +} + +tasks.named('compileTestJava') { + outputs.cacheIf { true } +} + // NOTE: Gradle suggested these dependencies compileTestJava.dependsOn(':fineract-provider:generateGitProperties', ':fineract-provider:processResources', ':fineract-provider:resolve') spotbugsTest.dependsOn(':fineract-provider:generateGitProperties', ':fineract-provider:processResources', ':fineract-provider:resolve') diff --git a/twofactor-tests/src/test/java/org/apache/fineract/twofactortests/TwoFactorAuthenticationTest.java b/twofactor-tests/src/test/java/org/apache/fineract/twofactortests/TwoFactorAuthenticationTest.java index 384bdd939b7..68e87685095 100644 --- a/twofactor-tests/src/test/java/org/apache/fineract/twofactortests/TwoFactorAuthenticationTest.java +++ b/twofactor-tests/src/test/java/org/apache/fineract/twofactortests/TwoFactorAuthenticationTest.java @@ -126,10 +126,10 @@ public void testGetTwofactorMethods() { @Test public void testTwofactorLogin() throws IOException, MessagingException { - assertEquals(greenMail.getReceivedMessages().length, 0); + assertEquals(0, greenMail.getReceivedMessages().length); performServerPost(requestSpec, responseSpec, "/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", ""); - assertEquals(greenMail.getReceivedMessages().length, 1); + assertEquals(1, greenMail.getReceivedMessages().length); Pattern p = Pattern.compile("token is (.+)."); Matcher m = p.matcher((CharSequence) greenMail.getReceivedMessages()[0].getContent()); @@ -161,10 +161,10 @@ public void testTwofactorLogin() throws IOException, MessagingException { @Test public void testTfaConfigSettings() throws IOException, MessagingException { - assertEquals(greenMail.getReceivedMessages().length, 0); + assertEquals(0, greenMail.getReceivedMessages().length); performServerPost(requestSpec, responseSpec, "/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", ""); - assertEquals(greenMail.getReceivedMessages().length, 1); + assertEquals(1, greenMail.getReceivedMessages().length); Pattern p = Pattern.compile("token is (.+)."); Matcher m = p.matcher((CharSequence) greenMail.getReceivedMessages()[0].getContent()); @@ -202,7 +202,7 @@ public void testTfaConfigSettings() throws IOException, MessagingException { // Login again performServerPost(requestSpec, responseSpec, "/fineract-provider/api/v1/twofactor?deliveryMethod=email&extendedToken=false&" + TENANT_IDENTIFIER, "", ""); - assertEquals(greenMail.getReceivedMessages().length, 2); + assertEquals(2, greenMail.getReceivedMessages().length); Matcher m2 = p.matcher((CharSequence) greenMail.getReceivedMessages()[1].getContent());