/*
 * Licensed 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.
 */
plugins {
  id 'com.eriwen.gradle.js' version '1.12.1'
  id 'com.github.ben-manes.versions' version '0.11.3'
  id 'com.github.hierynomus.license' version '0.11.0'
  id 'com.moowork.node' version '1.2.0'
  id 'me.champeau.gradle.jmh' version '0.4.4'
  id "com.github.spotbugs" version "1.6.4"
}

apply plugin: 'application'
apply plugin: 'checkstyle'
apply plugin: 'jacoco'
apply plugin: 'pmd'

def minJavaVersion = JavaVersion.VERSION_1_8;

allprojects {
  apply plugin: 'java'
  apply plugin: 'idea'
  apply plugin: 'maven-publish'
  apply plugin: 'project-report'

  buildDir = 'dist'

  repositories {
    mavenCentral()
  }

  compileJava {
    sourceCompatibility = minJavaVersion
    targetCompatibility = minJavaVersion
  }

  group 'org.apache.aurora'
  version = file("${rootDir}/.auroraversion").text.trim().toUpperCase()
  if (version.contains('/')) {
    throw new GradleException('''
*******************************************************************************
The application version string read by gradle is invalid.  This is known to
happen when building on Windows, or within vagrant with a Windows host.
You can work around this issue by cloning git _within_ vagrant and building
from there.

For more details, please see https://issues.apache.org/jira/browse/AURORA-1169
*******************************************************************************
''')
  }

  task sourceJar(type: Jar) {
    from sourceSets.main.allJava
  }

  if (project.hasProperty('internalMavenUrl')) {
    publishing {
      repositories {
        maven {
          credentials {
            username = internalMavenUser
            password = internalMavenPass
          }
          url internalMavenUrl
        }
      }
    }
  }

  ext.commonsLangRev = '2.6'
  ext.curatorRev = '2.12.0'
  ext.gsonRev = '2.3.1'
  ext.guavaRev = '23.2-jre'
  ext.guiceRev = '4.1.0'
  ext.httpclientRev = '4.5.2'
  ext.httpcoreRev = '4.4.4'
  ext.asyncHttpclientRev = '2.0.37'
  ext.jacksonRev = '2.5.1'
  ext.jaxRsRev = '2.0'
  ext.junitRev = '4.12'
  ext.logbackRev = '1.2.3'
  ext.nettyRev = '4.0.52.Final'
  ext.protobufRev = '3.5.1'
  ext.resteasyRev = '3.1.4.Final'
  ext.servletRev = '3.1.0'
  ext.slf4jRev = '1.7.25'
  ext.stringTemplateRev = '3.2.1'
  ext.thriftRev = '0.10.0'
  ext.zookeeperRev = '3.4.8'

  configurations.all {
    exclude module: 'junit-dep'
    // We use logback, so we do not want to pull in log4j artifacts.
    exclude module: 'log4j'
    exclude module: 'slf4j-log4j12'

    // ResolutionStrategy needs to be set in the allprojects block otherwise dependent projects
    // will not inherit it. Note that dependencies still need to be specified in a dependencies
    // block - this only affects strategy.
    // See http://forums.gradle.org/gradle/topics/shouldnt-resolutionstrategy-affect-depending-projects-transitive-dependencies
    resolutionStrategy {
      failOnVersionConflict()
      force "com.google.inject:guice:${guiceRev}"
      force "com.google.inject.extensions:guice-multibindings:${guiceRev}"
      force "org.apache.httpcomponents:httpclient:${httpclientRev}"
      force "org.apache.httpcomponents:httpcore:${httpcoreRev}"
      force "com.fasterxml.jackson.core:jackson-annotations:${jacksonRev}"
      force "com.fasterxml.jackson.core:jackson-core:${jacksonRev}"
      force "com.google.code.gson:gson:${gsonRev}"
      force "com.google.guava:guava:${guavaRev}"
      force "com.google.protobuf:protobuf-java:${protobufRev}"
      force "io.netty:netty-handler:${nettyRev}"
      force "junit:junit:${junitRev}"
      force "org.apache.thrift:libthrift:${thriftRev}"
      force "org.apache.zookeeper:zookeeper:${zookeeperRev}"
      force "org.hamcrest:hamcrest-core:1.3"
      force 'org.objenesis:objenesis:2.2'
      force "org.slf4j:slf4j-api:${slf4jRev}"
    }
  }
}

/**
 * This gradle project contains the logic to bootstrap and build the UI bundle using NodeJS.
 * The core logic of UI building is delegated to npm and Webpack. Webpack writes the UI bundle
 * into the resources directory of the Scheduler, so it must be built before the Scheduler.
 */
project(':ui') {
  apply plugin: 'com.moowork.node'
  node {
    download = true
    version = '12.14.1'
  }

  task test_(type: NpmTask, overwrite: true) {
    inputs.files(fileTree('src'), fileTree('plugin'))
    outputs.files(fileTree('dist'))
    args = ['test']
  }

  task pluginInstall(type: NpmTask) {
    inputs.files(file('plugin/package.json'))
    outputs.files(fileTree('node_modules'))

    args = ['install', 'plugin/']
  }

  task install(type: NpmTask, dependsOn: 'pluginInstall') {
    inputs.files(file('package.json'))
    outputs.files(fileTree('node_modules'))
    // Install into the project dir to sandbox everything under ui/
    args = ['install']
  }

  task lint(type: NpmTask, dependsOn: 'install') {
    inputs.files(fileTree('src'), fileTree('plugin'))
    outputs.files(fileTree('.'))
    args = ['run', 'lint']
  }

  task webpack(type: NodeTask, dependsOn: 'install') {
    inputs.files(fileTree('src'), fileTree('plugin'))
    outputs.files(sourceSets.main.java)
    script = file('node_modules/.bin/webpack')
  }

  tasks.build.dependsOn(lint)
  tasks.build.dependsOn(webpack)
  tasks.build.dependsOn(npm_test)
}
// Make sure UI webpack is run as part of 'processResources --continuous'. Also makes sure when
// building the Scheduler JAR, the JS bundle is built first.
tasks.processResources.dependsOn(':ui:webpack')

project(':commons') {
  apply plugin: 'license'
  license {
    header rootProject.file('config/checkstyle/apache.header')
    strictCheck true
  }

  dependencies {
    compile "com.google.code.gson:gson:${gsonRev}"
    compile "com.google.guava:guava:${guavaRev}"
    compile "com.google.inject:guice:${guiceRev}"
    compile "commons-lang:commons-lang:${commonsLangRev}"
    compile "javax.servlet:javax.servlet-api:${servletRev}"
    compile "javax.ws.rs:javax.ws.rs-api:${jaxRsRev}"
    compile "org.antlr:stringtemplate:${stringTemplateRev}"
    compile "org.apache.zookeeper:zookeeper:${zookeeperRev}"
    compile "org.easymock:easymock:3.4"

    // There are a few testing support libs in the src/main/java trees that use junit - currently:
    //   src/main/java/org/apache/aurora/common/zookeeper/testing
    //   src/main/java/org/apache/aurora/common/testing
    compile "junit:junit:${junitRev}"

    testCompile "junit:junit:${junitRev}"
  }
}

project(':api') {
  apply plugin: org.apache.aurora.build.ThriftPlugin
  apply plugin: org.apache.aurora.build.ThriftEntitiesPlugin

  task checkPython {
    doLast {
      def python27Executable = ['python2.7', 'python'].find { python ->
        try {
          def check = "import sys; sys.exit(0 if sys.version_info >= (2,7) and sys.version_info < (3,) else 1)"
          return [python, "-c", check].execute().waitFor() == 0
        } catch (IOException e) {
          return false
        }
      }

      if (python27Executable == null) {
        throw new GradleException('Build requires Python 2.7.')
      } else {
        thriftEntities.python = python27Executable
      }
    }
  }
  generateThriftEntitiesJava.dependsOn checkPython

  tasks.withType(Jar) {
    baseName "aurora-api"
  }

  publishing {
    publications {
      mavenJava(MavenPublication) {
        from components.java

        artifactId "aurora-api"

        artifact sourceJar {
          classifier "sources"
        }
      }
    }
  }

  thrift {
    version = thriftRev
    resourcePrefix = 'org/apache/aurora/scheduler/gen/client'
  }

  thriftEntities {
    gsonRev = project.gsonRev
    guavaRev = project.guavaRev
    inputFiles = fileTree("src/main/thrift/org/apache/aurora/gen").matching {
      include "**/*.thrift"
    }
  }

  idea {
    module {
      [thrift.genJavaDir, thriftEntities.genJavaDir].each {
        sourceDirs += it
        generatedSourceDirs += it
      }

      // These directories must exist, else the plugin omits them from the
      // generated project. Since this is executed during the configuration
      // lifecycle phase, dependency tasks have not yet run and created
      // the directories themselves.
      // By default, the idea module [1] excludes are set to
      // [project.buildDir, project.file('.gradle')]
      // This has the side-effect of also excluding our generated sources [2].  Due to the way
      // directory exclusion works in idea, you can't exclude a directory and include a child of that
      // directory. Clearing the excludes seems to have no ill side-effects, making it preferable to
      // other possible approaches.
      //
      // [1] http://www.gradle.org/docs/current/dsl/org.gradle.plugins.ide.idea.model.IdeaModule.html
      // [2] http://issues.gradle.org/browse/GRADLE-1174
      excludeDirs = [file(".gradle")]
      [
          "classes",
          "dependency-cache",
          "docs",
          "jacoco",
          "reports",
          "test-results",
          "tmp"
      ].each {
        excludeDirs << file("$buildDir/$it")
      }
    }
  }
}

def generatedDir = "$buildDir/generated-src"
def httpAssetsPath = 'scheduler/assets'

compileJava {
  options.compilerArgs << '-Werror'
  options.compilerArgs << '-Xlint:all'
  // Don't fail for annotations not claimed by annotation processors.
  options.compilerArgs << '-Xlint:-processing'
  // Don't fail for serialVersionUID warnings.
  options.compilerArgs << '-Xlint:-serial'
  // Capture method parameter names in classfiles.
  options.compilerArgs << '-parameters'
}

task enforceVersion {
  def foundVersion = JavaVersion.current();
  if (foundVersion < minJavaVersion) {
    throw new GradleException("Build requires at least Java ${minJavaVersion}; but ${foundVersion}"
        + " was found. Consider setting JAVA_HOME to select a specific JDK on your system.");
  }
}

compileJava.dependsOn(enforceVersion);

task wrapper(type: Wrapper) {
  gradleVersion = project(':buildSrc').GRADLE_VERSION
}

// TODO(ksweeney): Consider pushing this down to API - the scheduler implementation itself should
// only be consumed as an application.
publishing {
  publications {
    mavenJava(MavenPublication) {
      from components.java

      artifactId 'aurora-scheduler'

      artifact sourceJar {
        classifier "sources"
      }
    }
  }
}

task generateBuildProperties (type:Exec) {
  def outputDir = file("${buildDir}/build-properties")
  def outputFile = file("${outputDir}/build.properties")
  outputs.upToDateWhen { false }
  outputs.dir outputDir
  doFirst {
    outputDir.exists() || outputDir.mkdirs()
  }

  commandLine "${projectDir}/build-support/generate-build-properties", "${outputFile}"
}

sourceSets {
  main {
    output.dir generateBuildProperties
    resources {
      srcDir '3rdparty/javascript'
    }
  }
}

dependencies {
  def shiroRev = '1.4.0'
  def jettyDep = '9.3.11.v20160721'

  compile project(':api')
  compile project(':commons')

  compile 'aopalliance:aopalliance:1.0'
  compile "ch.qos.logback:logback-classic:${logbackRev}"
  compile "com.beust:jcommander:1.72"
  compile "com.google.inject:guice:${guiceRev}"
  compile "com.google.inject.extensions:guice-assistedinject:${guiceRev}"
  compile "com.google.inject.extensions:guice-multibindings:${guiceRev}"
  compile "com.google.inject.extensions:guice-servlet:${guiceRev}"
  compile "com.google.protobuf:protobuf-java:${protobufRev}"
  compile 'com.hubspot.jackson:jackson-datatype-protobuf:0.9.3'
  compile "com.fasterxml.jackson.core:jackson-core:${jacksonRev}"
  compile "org.jboss.resteasy:resteasy-guice:${resteasyRev}"
  compile "org.jboss.resteasy:resteasy-jackson-provider:${resteasyRev}"
  compile "org.jboss.resteasy:resteasy-jaxrs:${resteasyRev}"
  compile 'javax.inject:javax.inject:1'
  compile "javax.servlet:javax.servlet-api:${servletRev}"
  compile "org.antlr:stringtemplate:${stringTemplateRev}"
  compile "org.apache.curator:curator-client:${curatorRev}"
  compile "org.apache.curator:curator-framework:${curatorRev}"
  compile "org.apache.curator:curator-recipes:${curatorRev}"
  compile 'org.apache.mesos:mesos:1.6.1'
  compile "org.asynchttpclient:async-http-client:${asyncHttpclientRev}"
  compile "org.apache.shiro:shiro-guice:${shiroRev}"
  compile "org.apache.shiro:shiro-web:${shiroRev}"
  compile "org.apache.zookeeper:zookeeper:${zookeeperRev}"
  compile "org.eclipse.jetty:jetty-rewrite:${jettyDep}"
  compile "org.eclipse.jetty:jetty-server:${jettyDep}"
  compile "org.eclipse.jetty:jetty-servlet:${jettyDep}"
  compile "org.eclipse.jetty:jetty-servlets:${jettyDep}"
  compile 'org.quartz-scheduler:quartz:2.2.2'

  testCompile 'com.sun.jersey:jersey-client:1.19'
  testCompile "junit:junit:${junitRev}"
  testCompile "org.powermock:powermock-module-junit4:1.6.4"
  testCompile "org.powermock:powermock-api-easymock:1.6.4"
}

// For normal developer builds, avoid running the often-time-consuming code quality checks.
// Jenkins will always run these, and developers are encouraged to run these before posting diffs
// and pushing to master.
def runCodeQuality = project.hasProperty('q')
def codeQualityTasks = [
    Checkstyle,
    com.github.spotbugs.SpotBugsTask,
    nl.javadude.gradle.plugins.license.License,
    Pmd
]
codeQualityTasks.each {
  tasks.withType(it) {
    enabled = runCodeQuality
  }
}

checkstyle {
  sourceSets = [sourceSets.main , sourceSets.test, sourceSets.jmh]
  toolVersion = '7.3'
}

spotbugs {
  toolVersion = '3.1.0'
  effort = "max"
}

tasks.withType(com.github.spotbugs.SpotBugsTask) {
  reports {
    xml.enabled = false
    html.enabled = true
  }
  maxHeapSize = '1g'
  excludeFilter = rootProject.file('config/spotbugs/excludeFilter.xml')
}

pmd {
  toolVersion = '5.5.3'
  consoleOutput = true
}

// As recommended here to work around PMD bugs exposed by otherwise
// handing it an `auxClasspath` to resolve types with:
//   https://discuss.gradle.org/t/upgrading-gradle-from-2-7-to-2-8-results-in-pmd-false-positives/13460
tasks.withType(Pmd) {
  classpath = null
}

pmdMain {
  ruleSetFiles = files('config/pmd/common.xml', 'config/pmd/main.xml')
}

pmdTest {
  ruleSetFiles = files('config/pmd/common.xml', 'config/pmd/test.xml')
}

tasks.withType(Test) {
  maxParallelForks = Runtime.runtime.availableProcessors()
}

idea {
  project {
    vcs = 'Git'
    jdkName = '1.8'
    languageLevel = '1.8'

    ipr {
      withXml {
        def projectNode = it.asNode()

        // TODO(wfarner): Does the below work?  It still seems necessary to manually turn on
        // annotation processing.

        // Configure an annotation processor profile.
        def compilerConfiguration = projectNode.find {
          it.name() == 'component' && it.@name == 'CompilerConfiguration'
        }
        def apt = compilerConfiguration.find { it.name() == 'annotationProcessing' }
        // Turn on annotation processing only for the whitelisted modules.
        apt.replaceNode {
          annotationProcessing {
            profile(default: 'true', name: 'Default', enabled: 'false')
            profile(default: 'false', name: 'apt', enabled: 'true') {
              sourceOutputDir(name: 'generated')
              sourceTestOutputDir(name: 'generated')
              processorPath(useClasspath: 'true')
              module(name: 'aurora')
            }
          }
        }
        def projectRoot = projectNode.find {
          it.name() == 'component' && it.@name == 'ProjectRootManager'
        }
        projectRoot.remove(projectRoot.output)
      }
    }
  }
}

// Map output dirs explicitly per-module with the end goal of mapping the annotation processor
// generated code output dirs as source (and test) dirs.  The key difficulty being worked around
// here is the fact that excludes higher in a directory tree override includes deeper in the tree.
allprojects {
  def generatedSourceDir = file("${it.projectDir}/out/production/generated")
  def generatedTestDir = file("${it.projectDir}/out/test/generated")

  idea {
    module {
      generatedSourceDir.exists() || generatedSourceDir.mkdirs()
      sourceDirs += generatedSourceDir
      generatedSourceDirs += generatedSourceDir

      generatedTestDir.exists() || generatedTestDir.mkdirs()
      testSourceDirs += generatedTestDir
      generatedSourceDirs += generatedTestDir

      iml {
        withXml {
          def moduleNode = it.asNode()
          def moduleConfiguration = moduleNode.find {
            it.name() == 'component' && it.@name == 'NewModuleRootManager'
          }
          moduleConfiguration.attributes().remove('inherit-compiler-output')
          moduleConfiguration.appendNode('output', [url: 'file://$MODULE_DIR$/out/production'])
          moduleConfiguration.appendNode('output-test', [url: 'file://$MODULE_DIR$/out/test'])

          def excludeOutput = moduleConfiguration.find { it.name() == 'exclude-output' }
          if (excludeOutput) {
            moduleConfiguration.remove(excludeOutput)
          }
        }
      }
    }
  }
}

// Configuration parameters for the application plugin.
applicationName = 'aurora-scheduler'
mainClassName = 'org.apache.aurora.scheduler.app.SchedulerMain'

// TODO(ksweeney): Configure this to scan resources as well.
tasks.withType(nl.javadude.gradle.plugins.license.License).each {
  it.source = files("$projectDir/src/main/java", "$projectDir/src/test/java")
}

license {
  header rootProject.file('config/checkstyle/apache.header')
  strictCheck true
  skipExistingHeaders true
}

def reportPath = "$buildDir/reports/jacoco/test"
jacocoTestReport {
  group = "Reporting"
  description = "Generate Jacoco coverage reports after running tests."

  sourceDirectories = sourceSets.main.java
  classDirectories = files("$buildDir/classes/main")
  reports {
    xml.enabled true
  }
  doLast {
    println "Coverage report generated: file://$reportPath/html/index.html"
  }
}
test.finalizedBy jacocoTestReport

jacocoTestCoverageVerification {
  violationRules {
    rule {
      limit {
        counter = 'INSTRUCTION'
        minimum = 0.87
      }
    }
    rule {
      limit {
        counter = 'BRANCH'
        minimum = 0.79
      }
    }
  }
}
jacocoTestReport.finalizedBy jacocoTestCoverageVerification

def jmhHumanOutputPath = "$buildDir/reports/jmh/human.txt"
jmh {
  // Run specific benchmarks by passing -Pbenchmarks='<regexp' on the command line.  For example
  // ./gradlew jmh -Pbenchmarks='ThriftApiBenchmarks.*'
  if (project.hasProperty('benchmarks')) {
    include = project.getProperty('benchmarks')
  }
  jmhVersion = '1.15'
  jvmArgsPrepend = '-Xmx3g'
  humanOutputFile = project.file("$jmhHumanOutputPath")
  resultsFile = project.file("$buildDir/reports/jmh/results.txt")
  profilers = ['gc', 'stack']
}
tasks.getByName('jmh').doLast() {
  println "Benchmark report generated: file://$jmhHumanOutputPath"
}

run {
  main = 'org.apache.aurora.scheduler.app.local.LocalSchedulerMain'
  classpath += sourceSets.test.output
}

startScripts {
  def environmentClasspathPrefix = System.env.CLASSPATH_PREFIX

  if (!environmentClasspathPrefix?.trim()) {
    return
  }

  doLast {
    unixScript.text = unixScript.text.replace('CLASSPATH=', "CLASSPATH=${environmentClasspathPrefix}:")
  }
}

// Include a script to run the recovery tool.
task moreStartScripts(type: CreateStartScripts) {
  mainClassName = 'org.apache.aurora.scheduler.storage.durability.RecoveryTool'
  applicationName = 'recovery-tool'
  outputDir = new File(project.buildDir, 'scripts')
  classpath = jar.outputs.files + project.configurations.runtime
}

applicationDistribution.into('bin') {
  from(moreStartScripts)
  fileMode = 0755
}
