/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import org.apache.tools.ant.filters.ReplaceTokens
import org.asciidoctor.gradle.jvm.AsciidoctorTask

/* ========================================================
 * Project setup
 * ======================================================== */
plugins {
    id 'application' // plugins that are versioned as part of Gradle are using simple quotes to differentiate them
    id 'groovy'
    id 'eclipse'
    id 'checkstyle'
    id 'codenarc'
    id 'maven-publish'
    id "org.asciidoctor.jvm.convert" version "latest.release"
    id "org.asciidoctor.jvm.pdf" version "latest.release"
    id "org.owasp.dependencycheck" version "latest.release" apply false
    //id 'se.patrikerdes.use-latest-versions' version '0.2.18' apply false
    id "se.patrikerdes.use-latest-versions"  version "latest.release" apply false
    id "com.github.ben-manes.versions" version "latest.release" apply false
    id "com.github.ManifestClasspath" version "latest.release"
    id "com.github.jakemarsden.git-hooks" version "latest.release"
    id "com.github.node-gradle.node" version "latest.release" apply false
}

/* OWASP plugin
 *
 * If project property "enableOwasp" is flagged then
 * gradle will download required dependencies and
 * activate Gradle's OWASP plugin and its related tasks.
 *
 * Syntax: gradlew -PenableOwasp dependencyCheckAnalyze
 */
if (project.hasProperty('enableOwasp')) {
    apply plugin: 'org.owasp.dependencycheck'
}

/* DependencyUpdates plugin
 *
 * If project property "enableDependencyUpdates" is flagged then
 * gradle will download required dependencies and
 * activate Gradle's DependencyUpdates plugin and its related tasks.
 *
 * Syntax: gradlew -PenableDependencyUpdates dependencyUpdates -Drevision=release
 *
 * You may want to use the use-latest-versions plugin to help you in your work
 * Syntax:
 *  Check only: gradlew -PenableDependencyUpdates useLatestVersions && gradlew -PenableDependencyUpdates useLatestVersionsCheck
 *  Automated update: gradlew -PenableDependencyUpdates useLatestVersions
 *  Beware that this is only a help.
 *  If you use it without check you will need to check things by yourself (can be as tedious as not using this plugin)
 */
if (project.hasProperty('enableDependencyUpdates')) {
    apply plugin: 'com.github.ben-manes.versions'
    apply plugin: 'se.patrikerdes.use-latest-versions'
}

/* Configuration and default values. By default not available as it breacks Gradle build even when useLatestVersionsCheck (see above) is used.
useLatestVersions {
   // A whitelist of dependencies to update, in the format of group:name
   // Equal to command line: --update-dependency=[values]
   updateWhitelist = []
   // A blacklist of dependencies to update, in the format of group:name
   // Equal to command line: --ignore-dependency=[values]
   updateBlacklist = []
   // When enabled, root project gradle.properties will also be populated with
   // versions from subprojects in multi-project build
   // Equal to command line: --update-root-properties
   updateRootProperties = false
   // By default plugin tries to find all relevant gradle files (e.g. *.gradle, gradle.properties etc).
   // This can be slow in some cases when project has a lot of gradle files. For example when using conventions
   // in buildSrc. With this option you can specify what files should plugin search and check. Plugin will ignore
   // files that don't exist. Empty list means use default strategy. File paths are relative to project dir.
   //
   // Example:
   // versionFiles = ["gradle.build", "gradle.properties"]
   // Will check just $projectDir/gradle.build and $projectDir/gradle.properties
   //
   // Note:
   // You always have to specify file that has dependencies in some common dependency format with artifact coordinates,
   // e.g. compileOnly "group:module:version" or compileOnly("group:module:version") or val dependency = "group:module:version" etc.
   // For example if you set just versionFiles = ["gradle.properties"] this won't work, since plugin
   // won't be able to correlate variable with artifact coordinates.
   //
   // Equal to command line: --version-files=[values]
   versionFiles = []
   // List of root project files to update when updateRootProperties is enabled.
   // `build.gradle` is not an acceptable entry here as it breaks other expected
   // functionality. Version variables in `build.gradle` need to be moved into
   // a separate file which can be listed here.
   // Equal to command line: --root-version-files=[values]
   //rootVersionFiles = ['gradle.properties']
}*/

apply from: 'common.gradle'
apply from: 'dependencies.gradle'

// global properties
ext.os = System.getProperty('os.name').toLowerCase()
ext.gradlew = os.contains('windows') ? 'gradlew.bat' : './gradlew'
ext.pluginsDir = "${rootDir}/plugins"

application {
    mainClass = 'org.apache.ofbiz.base.start.Start'
    applicationDefaultJvmArgs = project.hasProperty('jvmArgs')
            ? jvmArgs.tokenize()
            : ['-Xms128M','-Xmx1024M',
            '-Djdk.serialFilter=maxarray=100000;maxdepth=20;maxrefs=1000;maxbytes=500000', // OFBIZ-12592 and OFBIZ-12716
//            '--add-exports=java.base/sun.util.calendar=ALL-UNNAMED', // OFBIZ-12721
            '--add-opens=java.base/java.util=ALL-UNNAMED' // OFBIZ-12726
            ]
}

distributions.main.contents.from(rootDir) {
    include 'framework/**', 'applications/**', 'themes/**', 'plugins/**'
}

javadoc {
    title="OFBiz " + getCurrentGitBranch() + " API"
    failOnError = true
    options {
        source '17'
        encoding 'UTF-8'
        charSet 'UTF-8'
        // Those external Javadoc links should correspond to the actual
        // versions declared in the 'dependencies' block.
        links(
            'https://docs.oracle.com/javase/17/docs/api',
            'https://tomcat.apache.org/tomcat-10.1-doc/servletapi/',
            'http://docs.groovy-lang.org/docs/groovy-4.0.22/html/api',
            'https://commons.apache.org/proper/commons-cli/apidocs'
        )
    }
    options as StandardJavadocDocletOptions
    //options.addStringOption('Xmaxwarns', '0') // Setting to 0 means all warnings (default 100). We have currently (2025/03/16) 12167 warnings!
}

java {
    sourceCompatibility(JavaVersion.VERSION_17)
    targetCompatibility(JavaVersion.VERSION_17)
}

// Java compile options, syntax gradlew -PXlint:none build
tasks.withType(JavaCompile) {
    options.encoding = 'UTF-8'
    if (!project.hasProperty('Xlint:none')) {
        options.compilerArgs << '-Xlint:all'
        // Exclude varargs warnings which are not silenced by @SafeVarargs.
        options.compilerArgs << '-Xlint:-varargs'
    }
}

// Only used for release branches
def getCurrentGitBranch() {
    return "git branch --show-current".execute().text.trim()
}

// defines the footer files for git info
def File gitFooterFile = file("${rootDir}/runtime/GitInfo.ftl")

// root and subproject settings
defaultTasks 'jar', 'test'

allprojects {
    repositories {
        mavenCentral()
        // the switch from jCenter to mavenCentral needs some additional repositories to be configured here
        // this should be checked frequently to remove obsolete configurations if the artifacts are available
        // on mavenCentral directly
        maven {
            // org.restlet and org.restlet.ext.servlet
            url 'https://maven.restlet.talend.com'
        }
        maven {
            // net.fortuna.ical4j:ical4j:1.0-rc3-atlassian-11
            url 'https://packages.atlassian.com/maven-3rdparty/'
        }
        maven {
            // org/milyn/flute/1.3/flute-1.3.jar
            // need artifact only because of wrong pom metadata in maven central
            // Required by: plugins:birt > org.eclipse.birt.runtime:viewservlets:4.5.0 > org.eclipse.birt.runtime:org.eclipse.birt.runtime:4.4.1
            // TODO Maybe this will no longer needed wheh upgrading viewservlets to 4.9.0
            url "https://repo1.maven.org/maven2"
            metadataSources {
                artifact()
            }
        }
        maven {
            url 'https://clojars.org/repo'
        }
        maven {
            // org.cyberneko.html.parsers (used by UELFunctions, was in esapi before 2.3)
            url "https://repository.ow2.org/nexus/content/repositories/public/"
        }
        maven {
            url "https://artifacts.alfresco.com/nexus/content/repositories/public/"
        }
        /* maven {
            // To test not released FreeMarker versions, see OFBIZ-12934 and sub-tasks for details
            url "https://repository.apache.org/content/repositories/snapshots/"
        } */
    }
}

subprojects {
    configurations {
        // compile-time plugin libraries
        pluginLibsCompile
        // runtime plugin libraries
        pluginLibsRuntime
        //compile-only libraries
        pluginLibsCompileOnly
    }
}

configurations {
    junitReport {
        description = 'libraries needed to run junitreport for OFBiz unit tests'
    }
    ofbizPlugins {
        description = 'ofbiz plugin dependencies configuration'
        transitive = true
    }
}

configurations.all {
    exclude group: 'log4j', module: 'log4j'
    exclude group: 'org.slf4j', module: 'slf4j-log4j12'
    exclude group: 'xml-apis', module: 'xml-apis'
    exclude group: 'jaxen', module: 'jaxen'
    exclude group: 'javax.xml.stream', module: 'stax-api'
    exclude group: 'org.apache.geronimo.specs', module: 'geronimo-stax-api_1.0_spec'
    exclude group: 'org.apache.geronimo.specs', module: 'geronimo-jta_1.1_spec'
    exclude group: 'javax.transaction', module: 'jta'
}

dependencies {
    // Dependencies defined by the plugins
    subprojects.each { subProject ->
        implementation project(path: subProject.path, configuration: 'pluginLibsCompile')
        runtimeOnly project(path: subProject.path, configuration: 'pluginLibsRuntime')
        compileOnly project(path: subProject.path, configuration: 'pluginLibsCompileOnly')
    }

    junitReport 'junit:junit:4.13.2'
    junitReport 'org.apache.ant:ant-junit:1.10.15'

    // Libraries downloaded manually
    implementation fileTree(dir: file("${rootDir}/lib"), include: '**/*.jar')
    getDirectoryInActiveComponentsIfExists('lib').each { libDir ->
        implementation fileTree(dir: libDir, include: '**/*.jar')
    }
}

def excludedJavaSources = [
    'org/apache/ofbiz/accounting/thirdparty/cybersource/IcsPaymentServices.java',
    'org/apache/ofbiz/accounting/thirdparty/orbital/OrbitalPaymentServices.java',
    'org/apache/ofbiz/accounting/thirdparty/paypal/PayPalServices.java',
    'org/apache/ofbiz/accounting/thirdparty/securepay/SecurePayPaymentServices.java',
    'org/apache/ofbiz/accounting/thirdparty/securepay/SecurePayServiceTest.java',
    'org/apache/ofbiz/accounting/thirdparty/verisign/PayflowPro.java',
    'org/apache/ofbiz/order/thirdparty/taxware/TaxwareException.java',
    'org/apache/ofbiz/order/thirdparty/taxware/TaxwareServices.java',
    'org/apache/ofbiz/order/thirdparty/taxware/TaxwareUTL.java'
]

sourceSets {
    // This is for integration tests (DB access)
    main {
        java {
            srcDirs = getDirectoryInActiveComponentsIfExists('src/main/java')
            exclude excludedJavaSources
        }
        groovy {
            srcDirs = getDirectoryInActiveComponentsIfExists('src/main/groovy')
        }
        resources {
            srcDirs = getDirectoryInActiveComponentsIfExists('src/main/resources')
            srcDirs += getDirectoryInActiveComponentsIfExists('config')
            srcDirs += getDirectoryInActiveComponentsIfExists('dtd')
        }
    }
    // This is for unit tests  (no DB access)
    test {
        java {
            srcDirs = getDirectoryInActiveComponentsIfExists('src/test/java')
        }
        // Groovy tests often fail, because JUNIT does not have access to the ofbiz enviroment.
        // If a groovy test is supposed to be tested this way, it can be added here.
        groovy {
            srcDirs = getDirectoryInActiveComponentsIfExists('src/test/groovy')
            include 'org/apache/ofbiz/service/ModelServiceTest.groovy'
            include 'org/apache/ofbiz/test/TestServices.groovy'
            include 'org/apache/ofbiz/base/util/string/FlexibleStringExpanderBaseCodeTests.groovy'
            include 'org/apache/ofbiz/base/util/FileUtilTests.groovy'
            include 'org/apache/ofbiz/service/test/ServicePurgeTest.groovy'
            include 'org.apache.ofbiz.base.test.SimpleTests.groovy'
        }
        resources {
            srcDirs = getDirectoryInActiveComponentsIfExists('src/test/resources')
        }
    }
}

jar.manifest.attributes(
    'Implementation-Title': project.name,
    'Main-Class': application.mainClass,
    'Class-Path': getJarClasspath()
)

// Checks OFBiz Java coding conventions.
checkstyle {
    // Defining a maximum number of "tolerated" errors ensures that
    // this number cannot increase in the future. It corresponds to
    // the sum of errors found last time it was changed after using the
    // 'checkstyle' tool present in the framework and in the official
    // plugins.
    tasks.checkstyleMain.maxErrors = 0
    // Increase memory for checkstyleMain required for Gradle 8+.
    tasks.checkstyleMain.maxHeapSize = '1g'
    // Currently there are no errors so we can show new one when they appear
    showViolations = true
    // Specify tool version so we can keep it up-to-date
    toolVersion = '10.20.2'
}
gitHooks {
    hooks = ['pre-push': 'checkstyleMain codenarcMain codenarcTest']
}

// Checks OFBiz Groovy coding conventions.
codenarc {
    setConfigFile(new File('config/codenarc/codenarc.groovy'))
    setMaxPriority1Violations(0)
    setMaxPriority2Violations(0)
    setMaxPriority3Violations(0)
}

// Eclipse plugin settings
eclipse.classpath.file.whenMerged { classpath ->
    /* The code inside this block removes unnecessary entries
     * in the .classpath file which are generated automatically
     * due to the settings in the sourceSets block
     */
    def fileSep = System.getProperty('file.separator')

    activeComponents().each { component ->
        def componentName = component.toString() - rootDir.toString() - fileSep
        def eclipseEntry = os.contains('windows') ? componentName.replaceAll("\\\\", '/') : componentName

        classpath.entries.removeAll { entry ->
            // remove any "src" entries in .classpath of the form /componentName
            entry.kind == 'src' && (
            entry.path ==~ '.*/+(' + componentName.tokenize(fileSep).last() + ')$' ||
            entry.path ==~ /(\/+framework)$/ ||
            entry.path ==~ /(\/+applications)$/ ||
            entry.path ==~ /(\/+plugins)$/ ||
            entry.path ==~ /(\/+themes)$/ )
        }
    }
}
tasks.eclipse.dependsOn(cleanEclipse)

test {
    jvmArgs "-javaagent:${classpath.find { it.name.contains('jmockit') }.absolutePath}"
}

/* ========================================================
 * Tasks
 * ======================================================== */

// ========== Task group labels ==========
def cleanupGroup = 'Cleaning'
def docsGroup = 'Documentation'
def ofbizServer = 'OFBiz Server'
def ofbizPlugin = 'OFBiz Plugin'
def sysadminGroup = 'System Administration'

// ========== OFBiz Server tasks ==========

task loadAll(group: ofbizServer) {
    dependsOn 'ofbiz --load-data'
    description 'Load default data; meant for OFBiz development, testing, and demo purposes'
}

task testIntegration(group: ofbizServer) {
    dependsOn 'ofbiz --test'
    description 'Run OFBiz integration tests; You must run loadAll before running this task'
}

task terminateOfbiz(group: ofbizServer,
    description: 'Force termination of any running OFBiz servers, only use if \"--shutdown\" command fails') {
    doLast {
        if (os.contains('windows')) {
            Runtime.getRuntime().exec("wmic process where \"CommandLine Like \'%org.apache.ofbiz.base.start.Start%\'\" Call Terminate")
        } else {
            def processOutput = new ByteArrayOutputStream()
            exec {
                commandLine 'ps', 'ax'
                standardOutput = processOutput
            }
            processOutput.toString().split(System.lineSeparator()).each { line ->
                if (line ==~ /.*org\.apache\.ofbiz\.base\.start\.Start.*/) {
                    exec { commandLine 'kill', '-9', line.tokenize().first() }
                }
            }
        }
    }
}

task loadAdminUserLogin(group: ofbizServer) {
    description 'Create admin user with temporary password equal to ofbiz. You must provide userLoginId'
    createOfbizCommandTask('executeLoadAdminUser',
        ['--load-data', 'file=/runtime/tmp/AdminUserLoginData.xml'])
    executeLoadAdminUser.doFirst {
        copy {
            from ("${rootDir}/framework/resources/templates/AdminUserLoginData.xml") {
                filter(ReplaceTokens, tokens: [userLoginId: userLoginId])
            }
            into "${rootDir}/runtime/tmp/"
        }
    }
    dependsOn executeLoadAdminUser
    doLast {
        delete("${rootDir}/runtime/tmp/AdminUserLoginData.xml")
    }
}

task loadTenant(group: ofbizServer, description: 'Load data using tenantId') {

    createOfbizCommandTask('executeLoadTenant', [])

    if (project.hasProperty('tenantId')) {
        executeLoadTenant.args '--load-data', "delegator=default#${tenantId}"
    }
    if (project.hasProperty('tenantReaders')) {
        executeLoadTenant.args '--load-data', "readers=${tenantReaders}"
    }
    if (project.hasProperty('tenantComponent')) {
        executeLoadTenant.args '--load-data', "component=${tenantComponent}"
    }

    executeLoadTenant.doFirst {
        if (!project.hasProperty('tenantId')) {
            throw new GradleException('Missing project property tenantId')
        }
    }
    dependsOn executeLoadTenant
}

task createTenant(group: ofbizServer, description: 'Create a new tenant in your environment') {

    def databaseTemplateFile = "${rootDir}/framework/resources/templates/AdminNewTenantData-Derby.xml"

    task prepareAndValidateTenantArguments {
        doLast {
            if (!project.hasProperty('tenantId')) {
                throw new GradleException('Project property tenantId is missing')
            }
            // dbPlatform values: D(Derby), M(MySQL), O(Oracle), P(PostgreSQL) (default D)
            if (project.hasProperty('dbPlatform')) {
                if (dbPlatform == 'D') {
                    databaseTemplateFile = "${rootDir}/framework/resources/templates/AdminNewTenantData-Derby.xml"
                } else if (dbPlatform == 'M') {
                    databaseTemplateFile = "${rootDir}/framework/resources/templates/AdminNewTenantData-MySQL.xml"
                } else if (dbPlatform == 'O') {
                    databaseTemplateFile = "${rootDir}/framework/resources/templates/AdminNewTenantData-Oracle.xml"
                } else if (dbPlatform == 'P') {
                    databaseTemplateFile = "${rootDir}/framework/resources/templates/AdminNewTenantData-PostgreSQL.xml"
                } else {
                    throw new GradleException('Invalid value for property dbPlatform: ' + "${dbPlatform}")
                }
            }
        }
    }

    task generateDatabaseTemplateFile(dependsOn: prepareAndValidateTenantArguments) {
        doLast {
            def filterTokens = ['tenantId': tenantId,
                'tenantName': project.hasProperty('tenantName') ? tenantName : tenantId,
                'domainName': project.hasProperty('domainName') ? domainName : 'org.apache.ofbiz',
                'db-IP': project.hasProperty('dbIp') ? dbIp : '',
                'db-User': project.hasProperty('dbUser') ? dbUser : '',
                'db-Password': project.hasProperty('dbPassword') ? dbPassword : '']

            generateFileFromTemplate(databaseTemplateFile, 'runtime/tmp',
                filterTokens, 'tmpFilteredTenantData.xml')
        }
    }

    task generateAdminUserTemplateFile(dependsOn: prepareAndValidateTenantArguments) {
        doLast {
            generateFileFromTemplate(
                "${rootDir}/framework/resources/templates/AdminUserLoginData.xml",
                'runtime/tmp',
                ['userLoginId': "${tenantId}-admin".toString()],
                'tmpFilteredUserLogin.xml')
        }
    }

    // Load the tenants master database
    createOfbizCommandTask('loadTenantOnMasterTenantDb',
        ['--load-data', 'file=/runtime/tmp/tmpFilteredTenantData.xml',
         '--load-data', 'readers=tenant'])
    loadTenantOnMasterTenantDb.dependsOn(generateDatabaseTemplateFile, generateAdminUserTemplateFile)

    // Load the actual tenant data
    createOfbizCommandTask('loadTenantData', [])
    loadTenantData.dependsOn(loadTenantOnMasterTenantDb)

    // Load the tenant admin user account
    createOfbizCommandTask('loadTenantAdminUserLogin', [])
    loadTenantAdminUserLogin.dependsOn(loadTenantData)

    /* pass arguments to tasks, must be done this way
     * because we are in the configuration phase. We cannot
     * set the parameters at the execution phase. */
    if (project.hasProperty('tenantId')) {
        loadTenantData.args '--load-data', "delegator=default#${tenantId}"
        loadTenantAdminUserLogin.args(
            '--load-data', "delegator=default#${tenantId}",
            '--load-data', "file=${rootDir}/runtime/tmp/tmpFilteredUserLogin.xml"
        )
    }
    if (project.hasProperty('tenantReaders')) {
        loadTenantData.args '--load-data', "readers=${tenantReaders}"
    }

    dependsOn(loadTenantAdminUserLogin)

    // cleanup
    doLast {
        delete("${rootDir}/runtime/tmp/tmpFilteredTenantData.xml")
        delete("${rootDir}/runtime/tmp/tmpFilteredUserLogin.xml")
    }
}

// ========== Documentation tasks ==========
tasks.withType(AsciidoctorTask) { task ->
    executionMode = JAVA_EXEC
        jvm {
            jvmArgs("--add-opens","java.base/sun.nio.ch=ALL-UNNAMED","--add-opens","java.base/java.io=ALL-UNNAMED")
        }
//    outputOptions {
//         // I hate we have to do this - but JRuby (asciidoctorj-pdf) and Windows don't mix well
//        if (System.properties['os.name'].toLowerCase().contains('windows')) {
//            backends = ['html5']
//        } else {
//            backends = ['html5', 'pdf']
//        }
         //It seems we have no choice when using Builbot we now get this issue:
//        Caused by: org.jruby.exceptions.LoadError: (LoadError) no such file to load -- asciidoctor-pdf
//        at org.jruby.RubyKernel.require(org/jruby/RubyKernel.java:1017)
//        at RUBY.require(uri:classloader:/META-INF/jruby.home/lib/ruby/stdlib/rubygems/core_ext/kernel_require.rb:85)
//        at RUBY.<main>(<script>:1)
//        backends = ['html5'] // this is the default, so for now outputOptions is not needed
//    }
    attributes \
        'doctype': 'book',
        'revnumber': getCurrentGitBranch(),
        'experimental': '',
        'allow-uri-read': true,
        'icons': 'font',
        'sectnums': '',
        'chapter-label': '',
        'toc': 'left@',
        'toclevels': '3'
}

task deleteOfbizDocumentation {
    doFirst { delete "${buildDir}/asciidoc/ofbiz" }
}

task deletePluginDocumentation {
    doFirst {
        if (!project.hasProperty('pluginId')) {
            throw new GradleException('Missing property \"pluginId\"')
        }
        if(!(activeComponents().stream().anyMatch { it.name == pluginId })) {
            throw new GradleException("Could not find plugin with id ${pluginId}")
        }
        delete "${buildDir}/asciidoc/plugins/${pluginId}"
    }
}

task deleteAllPluginsDocumentation {
    doFirst { delete "${buildDir}/asciidoc/plugins" }
}


task generateReadmeFiles(group: docsGroup, type: AsciidoctorTask) {
    doFirst { delete "${buildDir}/asciidoc/readme" }
    description 'Generate OFBiz README files'
    sourceDir "${rootDir}"
    // CHANGELOG.adoc should be only present in the current stable version
    sources {
        include 'README.adoc', 'CHANGELOG.adoc', 'CONTRIBUTING.adoc', 'DOCKER.adoc'
    }
    outputDir file("${buildDir}/asciidoc/readme/")
}

task generateOfbizDocumentation(group: docsGroup, type: AsciidoctorTask) {
    dependsOn deleteOfbizDocumentation
    description 'Generate OFBiz documentation manuals'
        activeComponents().each { component ->
            copy {
                from "${component}/src/docs/asciidoc/images/${component.name}"
                include '**/*.*'
                into "${rootDir}/docs/asciidoc/images/${component.name}"
            }
        }
    sourceDir "${rootDir}/docs/asciidoc"
    outputDir file("${buildDir}/asciidoc/ofbiz")
    doLast {
        activeComponents().each { component ->
            delete "${rootDir}/docs/asciidoc/images/${component.name}"
        }
    }
}

task generatePluginDocumentation(group: docsGroup) {
    dependsOn deletePluginDocumentation
    description 'Generate plugin documentation. Expects pluginId flag'
    activeComponents()
        .findAll { project.hasProperty('pluginId') && it.name == pluginId }
        .each { component ->
            def pluginAsciidoc = task "${component.name}Documentation" (type: AsciidoctorTask) {
                def asciidocFolder = new File("${component}/src/docs/asciidoc")
                if (asciidocFolder.exists()) {
                    copy {
                        from "${rootDir}/docs/asciidoc/images/OFBiz-Logo.svg"
                        into "${component}/src/docs/asciidoc/images"
                    }
                    sourceDir file("${component}/src/docs/asciidoc")
                    outputDir file("${buildDir}/asciidoc/plugins/${component.name}")
                    doLast { println "Documentation generated for plugin ${component.name}" }
                } else {
                    println "No documentation found for plugin ${component.name}"
                }
                doLast { delete "${component}/src/docs/asciidoc/images/OFBiz-Logo.svg" }
                mustRunAfter deletePluginDocumentation
            }
            dependsOn pluginAsciidoc
            doLast { delete "${component}/src/docs/asciidoc/images/OFBiz-Logo.svg" }
        }
}

task generateAllPluginsDocumentation(group: docsGroup,
        description: 'Generate all plugins documentation.') {

    dependsOn deleteAllPluginsDocumentation
    file("${pluginsDir}").eachDir { plugin ->
        activeComponents().each { component ->
            if (component.name == plugin.name) {
                if (subprojectExists(":plugins:${plugin.name}")) {
                    // Note: the "-" between "component.name" and "Documentation" allows to differentiate from
                    // the other inner task temporary created by the generatePluginDocumentation task
                    def pluginAsciidoc = task "${component.name}-Documentation" (type: AsciidoctorTask) {
                        def asciidocFolder = new File("${component}/src/docs/asciidoc")
                        doFirst {
                            if (asciidocFolder.exists()) {
                                copy {
                                    from "${rootDir}/docs/asciidoc/images/OFBiz-Logo.svg"
                                    into "${component}/src/docs/asciidoc/images"
                                }
                            }
                        }
                        if (asciidocFolder.exists()) {
                            sourceDir file("${component}/src/docs/asciidoc")
                            outputDir file("${buildDir}/asciidoc/plugins/${component.name}")
                            doLast { println "Documentation generated for plugin ${component.name}" }
                        }
                    mustRunAfter deleteAllPluginsDocumentation
                    doLast { delete "${component}/src/docs/asciidoc/images/OFBiz-Logo.svg" }
                    }
                    dependsOn pluginAsciidoc
                }
            }
        }
    }
}


// ========== System Administration tasks ==========
task createTestReports(group: sysadminGroup, description: 'Generate HTML reports from junit XML output') {
    doLast {
        ant.taskdef(name: 'junitreport',
            classname: 'org.apache.tools.ant.taskdefs.optional.junit.XMLResultAggregator',
            classpath: configurations.junitReport.asPath)
        ant.junitreport(todir: './runtime/logs/test-results') {
            fileset(dir: './runtime/logs/test-results') {
                include(name: '*.xml')
            }
            report(format:'frames', todir:'./runtime/logs/test-results/html')
        }
    }
}

task gitInfoFooter(group: sysadminGroup, description: 'Update the Git Branch-revision info in the footer if Git is used') {
    doLast {
        def branch
        def revision
        def timestamp = new Date().format 'yyyy-MM-dd HH:mm:ss'
        def gitFolder = rootProject.file('.git')

        if (!gitFolder.exists()) {
            println ('Git is not used')
            return
        }

        def branchOutput = new ByteArrayOutputStream()
        exec {
            commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
            standardOutput = branchOutput
        }
        branch = branchOutput.toString()
        def revisionOutput = new ByteArrayOutputStream()
        exec {
            commandLine 'git', 'rev-parse', 'HEAD'
            standardOutput = revisionOutput
        }
        revision = revisionOutput.toString()
        gitFooterFile.delete()
        gitFooterFile.createNewFile()
        gitFooterFile <<  System.lineSeparator()
        gitFooterFile << '${uiLabelMap.CommonBranch} : ' + "${branch}" + System.lineSeparator()
        gitFooterFile << '${uiLabelMap.CommonRevision} : ' + "${revision}" + System.lineSeparator()
        gitFooterFile << '${uiLabelMap.CommonBuiltOn} : ' + "${timestamp}" + System.lineSeparator()
        gitFooterFile << '${uiLabelMap.CommonJavaVersion} : ' + "${org.gradle.internal.jvm.Jvm.current()}"
    }
}

// ========== OFBiz Plugin Management ==========
task createPlugin(group: ofbizPlugin, description: 'create a new plugin component based on specified templates') {
    doLast {
        if (!project.hasProperty('pluginResourceName')) {
            ext.pluginResourceName = pluginId.capitalize()
        }
        if (!project.hasProperty('webappName')) {
            ext.webappName = pluginId
        }
        if (!project.hasProperty('basePermission')) {
            ext.basePermission = pluginId.toUpperCase()
        }

        def filterTokens = ['component-name': pluginId,
            'component-resource-name': pluginResourceName,
            'webapp-name': webappName,
            'base-permission': basePermission]
        def templateDir = "${rootDir}/framework/resources/templates"
        def pluginDir = "${pluginsDir}/${pluginId}"

        ['config', 'dtd', 'entitydef', 'lib', 'patches/test', 'patches/qa',
                'patches/production', 'src/main/groovy', 'minilang', 'servicedef', 'src/main/java', 'src/test/java', 'testdef',
                'widget', "webapp/${webappName}/error", "webapp/${webappName}/WEB-INF"].each {
            mkdir pluginDir + '/' + it
        }

        [   [tempName:'ofbiz-component.xml', newName:'ofbiz-component.xml', location:''],
            [tempName:'build.gradle', newName:'build.gradle', location:''],
            [tempName:'TypeData.xml', newName:"${pluginResourceName}TypeData.xml", location:'data'],
            [tempName:'SecurityPermissionSeedData.xml', newName:"${pluginResourceName}SecurityPermissionSeedData.xml", location:'data'],
            [tempName:'SecurityGroupDemoData.xml', newName:"${pluginResourceName}SecurityGroupDemoData.xml", location:'data'],
            [tempName:'DemoData.xml', newName:"${pluginResourceName}DemoData.xml", location:'data'],
            [tempName:'HELP.adoc', newName:'HELP.adoc', location:''],
            [tempName:'README.adoc', newName:'README.adoc', location:''],
            [tempName:'entitymodel.xml', newName:'entitymodel.xml', location:'entitydef'],
            [tempName:'services.xml', newName:'services.xml', location:'servicedef'],
            [tempName:'Tests.xml', newName:"${pluginResourceName}Tests.xml", location:'testdef'],
            [tempName:'UiLabels.xml', newName:"${pluginResourceName}UiLabels.xml", location:'config'],
            [tempName:'index.jsp', newName:'index.jsp', location:"webapp/${webappName}"],
            [tempName:'controller.xml', newName:'controller.xml', location:"webapp/${webappName}/WEB-INF"],
            [tempName:'web.xml', newName:'web.xml', location:"webapp/${webappName}/WEB-INF"],
            [tempName:'CommonScreens.xml', newName:'CommonScreens.xml', location:'widget'],
            [tempName:'Screens.xml', newName:"${pluginResourceName}Screens.xml", location:'widget'],
            [tempName:'Menus.xml', newName:"${pluginResourceName}Menus.xml", location:'widget'],
            [tempName:'Forms.xml', newName:"${pluginResourceName}Forms.xml", location:'widget']
        ].each { tmpl ->
            generateFileFromTemplate(templateDir + '/' + tmpl.tempName,
                pluginDir + '/' + tmpl.location, filterTokens, tmpl.newName)
        }

        println "plugin successfully created in directory ${pluginsDir}/${pluginId}."
    }
}

task installPlugin(group: ofbizPlugin, description: 'executes plugin install task if it exists') {

    doFirst {
        if (!project.hasProperty('pluginId')) {
            throw new GradleException('Missing property \"pluginId\"')
        }
    }

    if (project.hasProperty('pluginId')) {
        activeComponents()
            .findAll { it.name == pluginId }
            .each { component ->
                if (taskExistsInproject(":plugins:${pluginId}", 'install')) {
                    dependsOn ":plugins:${pluginId}:install"
                    doLast { println "installed plugin ${pluginId}" }
                } else {
                    doLast { println "No install task defined for plugin ${pluginId}, nothing to do" }
                }
            }
    }
}

task uninstallPlugin(group: ofbizPlugin, description: 'executes plugin uninstall task if it exists') {

    doFirst {
        if (!project.hasProperty('pluginId')) {
            throw new GradleException('Missing property \"pluginId\"')
        }
        if (!subprojectExists(":plugins:${pluginId}")) {
            throw new GradleException("Plugin \"${pluginId}\" does not exist")
        }
    }

    if (project.hasProperty('pluginId') && taskExistsInproject(":plugins:${pluginId}", 'uninstall')) {
        dependsOn ":plugins:${pluginId}:uninstall"
        doLast { println "uninstalled plugin ${pluginId}" }
    } else {
        doLast { println "No uninstall task defined for plugin ${pluginId}" }
    }
}

task removePlugin(group: ofbizPlugin, description: 'Uninstall a plugin and delete its files') {
    if (project.hasProperty('pluginId') && subprojectExists(":plugins:${pluginId}")) {
        dependsOn uninstallPlugin
    }

    doLast {
        if (file("${pluginsDir}/${pluginId}").exists()) {
            delete "${pluginsDir}/${pluginId}"
        } else {
            throw new GradleException("Directory not found: ${pluginsDir}/${pluginId}")
        }
    }
}

task pushPlugin(group: ofbizPlugin, description: 'push an existing plugin to local maven repository') {

    if (project.hasProperty('pluginId')) {
        doFirst {
            if (!subprojectExists(":plugins:${pluginId}")) {
                throw new GradleException("Plugin ${pluginId} does not exist, cannot publish")
            }
        }
        task createPluginArchive(type: Zip) {
            from "${pluginsDir}/${pluginId}"
        }

        publishing {
            publications {
                ofbizPluginPublication(MavenPublication) {
                    artifactId pluginId
                    groupId project.hasProperty('pluginGroup') ? pluginGroup : 'org.apache.ofbiz.plugin'
                    version project.hasProperty('pluginVersion') ? pluginVersion : '0.1.0-SNAPSHOT'

                    artifact createPluginArchive

                    pom.withXml {
                        if (project.hasProperty('pluginDescription')) {
                            asNode().appendNode('description', pluginDescription)
                        } else {
                            asNode().appendNode('description', "Publication of OFBiz plugin ${pluginId}")
                        }
                    }
                }
            }
        }

        if (subprojectExists(":plugins:${pluginId}")) {
            dependsOn publishToMavenLocal
        }

    } else {
        doFirst { throw new GradleException('Missing property \"pluginId\"') }
    }
}

task pullPlugin(group: ofbizPlugin, description: 'Download and install a plugin with all dependencies') {
    doLast {
        if (!project.hasProperty('dependencyId')) {
            throw new GradleException('You must pass the dependencyId of the plugin')
        }

        // Connect to a remote maven repository if defined
        if (project.hasProperty('repoUrl')) {
            repositories {
                maven {
                    url repoUrl
                    if (project.hasProperty('repoUser') && project.hasProperty('repoPassword')) {
                        credentials {
                            username repoUser
                            password repoPassword
                        }
                    }
                }
            }
        }

        // download plugin and dependencies
        dependencies {
            ofbizPlugins dependencyId
        }

        // reverse the order of dependencies to install them before the plugin
        def ofbizPluginArchives = new ArrayList(configurations.ofbizPlugins.files)
        Collections.reverse(ofbizPluginArchives)

        // Extract and install plugin and dependencies
        ofbizPluginArchives.each { pluginArchive ->
            ext.pluginId = dependencyId.tokenize(':').get(1)
            println "installing plugin: ${pluginId}"
            copy {
                from zipTree(pluginArchive)
                into "${pluginsDir}/${pluginId}"
            }
            gradlewSubprocess(['installPlugin', "-PpluginId=${pluginId}"])
        }
    }
}

// ========== Clean up tasks ==========
task cleanCatalina(group: cleanupGroup, description: 'Clean Catalina data in runtime/catalina/work') {
    doLast { delete "${rootDir}/runtime/catalina/work" }
}
task cleanData(group: cleanupGroup, description: 'Clean all DB data (Derby) under runtime/data') {
    doLast { deleteAllInDirWithExclusions("${rootDir}/runtime/data/", ['README', 'derby.properties']) }
}
task cleanDownloads(group: cleanupGroup, description: 'Clean all downloaded files') {
    doLast {
        delete fileTree(dir: "${rootDir}/framework/base/lib", includes: ['activemq-*.jar'])
        delete fileTree(dir: "${rootDir}/framework/entity/lib/jdbc", includes: ['postgresql-*.jar'])
        delete fileTree(dir: "${rootDir}/framework/entity/lib/jdbc", includes: ['mysql-*.jar'])
    }
}
task cleanLogs(group: cleanupGroup, description: 'Clean all logs in runtime/logs') {
    doLast { deleteAllInDirWithExclusions("${rootDir}/runtime/logs/", ['README']) }
}
task cleanOutput(group: cleanupGroup, description: 'Clean runtime/output directory') {
    doLast { deleteAllInDirWithExclusions("${rootDir}/runtime/output/", ['README']) }
}
task cleanIndexes(group: cleanupGroup, description: 'Remove search indexes (e.g. Lucene) from runtime/indexes') {
    doLast { deleteAllInDirWithExclusions("${rootDir}/runtime/indexes/", ['README', 'index.properties']) }
}
task cleanTempfiles(group: cleanupGroup, description: 'Remove file in runtime/tempfiles') {
    doLast {
        deleteAllInDirWithExclusions("${rootDir}/runtime/tempfiles/", ['README'])
        deleteAllInDirWithExclusions("${rootDir}/runtime/tmp/", ['README'])
    }
}
task cleanUploads(group: cleanupGroup, description: 'Remove uploaded files.') {
    doLast { deleteAllInDirWithExclusions("${rootDir}/runtime/uploads/", []) }
}
task cleanXtra(group: cleanupGroup, description: 'Clean extra generated files like .rej, .DS_Store, etc.') {
    doLast {
        delete fileTree(dir: "${rootDir}",
            includes: ['**/.nbattrs', '**/*~','**/.#*', '**/.DS_Store', '**/*.rej', '**/*.orig'])
    }
}
task cleanFooterFiles(group: cleanupGroup, description: 'clean generated footer files') {
    doLast {
        delete gitFooterFile
    }
}
/*
 * Keep this task below all other clean tasks  The location of
 * declaration is important because it means that it will automatically
 * run whenever the task cleanAll executes (dependency matched by regex)
 */
def cleanTasks = tasks.findAll { it.name ==~ /^clean.+/ }
task cleanAll(group: cleanupGroup, dependsOn: [cleanTasks, clean]) {
    description 'Execute all cleaning tasks.'
}

/* ========================================================
 * Rules-based OFBiz server commands
 * ======================================================== */

tasks.addRule('Pattern: ofbiz <Commands>: Execute OFBiz startup commands') { String taskName ->
    if (taskName ==~ /^ofbiz\s.*/ || taskName == 'ofbiz') {
        def arguments = (taskName - 'ofbiz').tokenize(' ')
        createOfbizCommandTask(taskName, arguments)
    }
}

tasks.addRule('Pattern: ofbizBackground <Commands>: Execute OFBiz startup commands in background and output to console.log') { String taskName ->
    if (taskName ==~ /^ofbizBackground\s.*/ || taskName == 'ofbizBackground') {
        createOfbizBackgroundCommandTask(taskName)
    }
}

/* ========================================================
 * Helper Functions
 * ======================================================== */

def createOfbizCommandTask(taskName, arguments) {
    task(type: JavaExec, dependsOn: classes, taskName) {
        jvmArgs(application.applicationDefaultJvmArgs)
        classpath = sourceSets.main.runtimeClasspath
        mainClass = application.mainClass
        args arguments
        if (taskName ==~ /^ofbiz.*(--test|-t).*/) {
            finalizedBy(createTestReports)
        }
    }
}

def createOfbizBackgroundCommandTask(taskName) {
    def sourceTask = taskName.tokenize().first()
    def arguments = (taskName - sourceTask)
    def serverCommand = "ofbiz ${arguments}".toString()

    task (taskName) {
        doLast {
            spawnProcess([gradlew, '--no-daemon', serverCommand])
        }
    }
}

def spawnProcess(command) {
    ProcessBuilder pb = new ProcessBuilder(command)
    File consoleLog = file("${rootDir}/runtime/logs/console.log")

    pb.directory(file("${rootDir}"))
    pb.redirectErrorStream(true)
    pb.redirectOutput(ProcessBuilder.Redirect.appendTo(consoleLog))
    pb.start()
}

def getDirectoryInActiveComponentsIfExists(String dirName) {
    activeComponents()
        .collect { file(it.toString() + '/' + dirName) }
        .findAll { it.exists() }
}

def deleteAllInDirWithExclusions(dirName, exclusions) {
    ant.delete (includeEmptyDirs: 'true', verbose: 'on') {
        fileset(dir: dirName, includes: '**/*', erroronmissingdir: 'false') {
            exclusions.each { exclusion ->
                exclude name: exclusion
            }
        }
    }
}

def generateFileFromTemplate(templateFileInFullPath, targetDirectory, filterTokens, newFileName) {
    copy {
        from (templateFileInFullPath) {
            filter ReplaceTokens, tokens: filterTokens
            rename templateFileInFullPath.tokenize('/').last(), newFileName
        }
        into targetDirectory
    }
}

def getJarClasspath() {
    def mapper = os.contains('windows') ? { '\\' + "$it" } : { "$it" }
    configurations.runtimeClasspath.collect(mapper).join(' ')
}

def subprojectExists(fullyQualifiedProject) {
    subprojects.stream()
        .anyMatch { it.path == fullyQualifiedProject.toString() }
}

def taskExistsInproject(fullyQualifiedProject, taskName) {
    subprojects.stream()
        .filter { it.path == fullyQualifiedProject.toString() }
        .flatMap { it.tasks.stream() }
        .anyMatch { it.name == taskName }
}

def gradlewSubprocess(commandList) {
    exec { commandLine(gradlew, '--no-daemon', *commandList) }
}


def gradle = project.getGradle()
new File("${gradle.getGradleUserHomeDir().getAbsolutePath()}/daemon/${gradle.getGradleVersion()}").listFiles().each {
    if (it.getName().endsWith('.out.log')) {
        logger.debug("Cleaning up daemon log file $it")
        it.delete()
    }
}

tasks.startScripts {
    doLast {
        // Alter the start script for Unix systems.
        unixScript.text =
                unixScript.text.replace('CLASSPATH=$APP_HOME/lib','CLASSPATH=$APP_HOME/config/:$APP_HOME/lib-extra/*:$APP_HOME/lib')
        // Alter the start script for Windows systems.
        windowsScript.text =
                windowsScript.text.replace('CLASSPATH=%APP_HOME%\\lib','CLASSPATH=%APP_HOME%\\conf\\;%APP_HOME%\\lib-extra\\*;%APP_HOME%\\lib')
    }
}
