/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
 */

/**
 * Support for modules that contain Java code
 */

buildscript {
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath buildscriptLibs.forbiddenapis
	}
}

import de.thetaphi.forbiddenapis.gradle.CheckForbiddenApis
import org.apache.tools.ant.filters.ReplaceTokens

apply plugin: 'java-library'

apply from: rootProject.file( 'gradle/module.gradle' )
apply from: rootProject.file( 'gradle/databases.gradle' )
apply from: rootProject.file( 'gradle/javadoc.gradle' )

apply plugin: 'biz.aQute.bnd.builder'
apply plugin: 'org.hibernate.orm.database-service'
apply plugin: 'org.hibernate.orm.build.java-module'

apply plugin: 'org.checkerframework'
apply plugin: 'de.thetaphi.forbiddenapis'
apply plugin: 'com.diffplug.spotless'

apply plugin: "jacoco"
apply plugin: 'build-dashboard'
apply plugin: 'project-report'


// Attempt to leverage JetBrain's Gradle extension to automatically define
// `copyResourcesToIntelliJOutFolder` as a "build trigger" on import.
//
// However, see https://github.com/JetBrains/gradle-idea-ext-plugin/issues/8

apply plugin: 'org.jetbrains.gradle.plugin.idea-ext'

ext {
	java9ModuleNameBase = project.name.startsWith( 'hibernate-' ) ? name.drop( 'hibernate-'.length() ): name
	java9ModuleName = "org.hibernate.orm.$project.java9ModuleNameBase".replace('-','.')
	forbiddenAPITargetJDKCompatibility = '11'
}

if ( !project.description ) {
	project.description = "The Hibernate ORM $project.name module"
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Reproducible Builds

// https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives
// Configure archive tasks to produce reproducible archives:
tasks.withType(AbstractArchiveTask).configureEach {
	preserveFileTimestamps = false
	reproducibleFileOrder = true
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Configurations and Dependencies

configurations.configureEach {
	resolutionStrategy.eachDependency { details ->
		//Force the "byte buddy agent" version to match the Byte Buddy version
		// we use, as Mockito might pull in a mismatched version transitively
		if (details.requested.group == "net.bytebuddy" && details.requested.name == 'byte-buddy-agent') {
			details.useVersion libs.versions.byteBuddy.get()
		}
	}
}

dependencies {
	implementation libs.logging

	compileOnly libs.loggingAnnotations
	// Used for compiling some Oracle specific JdbcTypes
	compileOnly jdbcLibs.oracle

	// JUnit dependencies made up of:
	// 		* JUnit 5
	// 		* the Jupiter engine which runs JUnit 5 based tests
	//		* the "vintage" engine - which runs JUnit 3 and 4 based tests
	testImplementation testLibs.junit5Api
	testImplementation testLibs.junit5Engine
	testImplementation testLibs.junit5Params
	testImplementation testLibs.junit4
	testImplementation testLibs.junit4Engine
	testImplementation testLibs.assertjCore

	testRuntimeOnly testLibs.log4j2
	testRuntimeOnly libs.byteBuddy

	//Databases
	testRuntimeOnly jdbcLibs.h2
	testRuntimeOnly jdbcLibs.derby
	testRuntimeOnly jdbcLibs.derbyTools
	testRuntimeOnly jdbcLibs.hsqldb
	testRuntimeOnly jdbcLibs.postgresql
	testRuntimeOnly jdbcLibs.mssql
	testRuntimeOnly jdbcLibs.informix
	testRuntimeOnly jdbcLibs.cockroachdb
	testRuntimeOnly jdbcLibs.sybase
	testRuntimeOnly rootProject.fileTree(dir: 'drivers', include: '*.jar')

	// Since both the DB2 driver and HANA have a package "net.jpountz" we have to add dependencies conditionally
	// This is due to the "no split-packages" requirement of Java 9+

	if ( db.startsWith( 'db2' ) ) {
		testRuntimeOnly jdbcLibs.db2
	}
	else if ( db.startsWith( 'hana' ) ) {
		testRuntimeOnly jdbcLibs.hana
	}
	else if ( db.startsWith( 'mysql' ) || db.startsWith( 'tidb' ) ) {
		testRuntimeOnly jdbcLibs.mysql
	}
	else if ( db.startsWith( 'mariadb' ) ) {
		testRuntimeOnly jdbcLibs.mariadb
	}
	else if ( db.startsWith( 'firebird' ) ) {
		testRuntimeOnly jdbcLibs.firebird
	}
	else if ( db.startsWith( 'oracle' ) ) {
		testRuntimeOnly jdbcLibs.oracle
		testRuntimeOnly jdbcLibs.oracleXml
		testRuntimeOnly jdbcLibs.oracleXmlParser
	}
	else if ( db.startsWith( 'altibase' ) ) {
		testRuntimeOnly jdbcLibs.altibase
	}
	else if ( db.startsWith( 'informix' ) ) {
		testRuntimeOnly jdbcLibs.informix
	}

	annotationProcessor libs.loggingProcessor
	annotationProcessor libs.logging
	annotationProcessor libs.loggingAnnotations


	constraints {
		implementation('org.apache.logging.log4j:log4j-core') {
			version {
				strictly('[2.17.1, 3[')
				prefer('2.17.1')
			}
			because('CVE-2021-44228, CVE-2021-45046, CVE-2021-45105, CVE-2021-44832: Log4j vulnerable to remote code execution and other critical security vulnerabilities')
		}
	}
}

configurations {
	javadocSources {
		canBeConsumed = true
		canBeResolved = false
		visible = false
		description = 'Configuration for accessing the sources that should be included in the javadoc for the project'
	}
}

artifacts {
	sourceSets.main.allJava.srcDirs.each { srcDir ->
		javadocSources srcDir
	}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Compilation

tasks.withType( JavaCompile ) {
	options.encoding = 'UTF-8'
	options.warnings false
	options.fork = true
	options.forkOptions.memoryMaximumSize = '768m'

	options.compilerArgs += [
			// disable adding @Generated annotation in the logger impls to make
			// the logging annotation processor create the same sources each time.
			"-Aorg.jboss.logging.tools.addGeneratedAnnotation=false"
//			"-nowarn",
//			"-encoding", "UTF-8"
	]
}

tasks.compileJava.dependsOn spotlessApply

task compile(dependsOn: [compileJava, processResources, compileTestJava, processTestResources] )



// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attach tools JAR to the classpath for byteman tests

final File toolsJar = file("${System.getProperty('java.home')}/../lib/tools.jar")
if ( toolsJar.exists() ) {
	dependencies{
		testImplementation files( toolsJar )
	}
}



// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Testing
class HeapDumpPathProvider implements CommandLineArgumentProvider {
	@OutputDirectory
	Provider<Directory> path

	@Override
	Iterable<String> asArguments() {
		["-XX:HeapDumpPath=${path.get().asFile.absolutePath}"]
	}
}

tasks.withType( Test.class ).each { test ->
	test.useJUnitPlatform()

	test.usesService( project.gradle.sharedServices.registrations.getByName( 'databaseService' ).service )

	// Byteman needs this property to be set, https://developer.jboss.org/thread/274997
	test.jvmArgs += ["-Djdk.attach.allowAttachSelf=true"]

	test.jvmArgumentProviders.add(
			new HeapDumpPathProvider( path: project.layout.buildDirectory.dir("OOM-dump") )
	)

	test.jvmArgs += [
			'-XX:+HeapDumpOnOutOfMemoryError',
			'-XX:MetaspaceSize=256M'
	]

	test.maxHeapSize = '3G'

	test.systemProperties['hibernate.test.validatefailureexpected'] = true
	test.systemProperties['hibernate.highlight_sql'] = false
	test.systemProperties += System.properties.findAll { it.key.startsWith( "hibernate." ) }

	test.enableAssertions = true

	if ( project.name != 'hibernate-testing' ) {
		test.dependsOn ':hibernate-testing:test'
	}

	// Allow to exclude specific tests
	if ( project.hasProperty( 'excludeTests' ) ) {
		test.filter {
			excludeTestsMatching project.property('excludeTests').toString()
		}
	}
}

sourceSets {
	test {
		resources {
			configure( srcDir('src/test/resources') ) {
				filter {
					include '*.properties'
					include '*.xml'
					include '**/*.properties'
					include '**/*.xml'
				}
			}
		}
	}
}


processTestResources {
	duplicatesStrategy DuplicatesStrategy.INCLUDE
	inputs.property( "db", db )
	inputs.property( "dbHost", dbHost )
	inputs.file( rootProject.file( "gradle/databases.gradle" ) )
	doLast {
		copy {
			from( sourceSets.test.java.srcDirs ) {
				include '**/*.properties'
				include '**/*.xml'
			}
			into sourceSets.test.java.classesDirectory
		}
		copy {
			from file( 'src/test/resources' )
			into file( "${buildDir}/resources/test" )
			exclude 'src/test/resources/hibernate.properties'
		}
		copy {
			from file( 'src/test/resources/hibernate.properties' )
			into file( "${buildDir}/resources/test" )
			filter( ReplaceTokens, tokens: dbBundle[db] )
		}
	}
}

// Keep system properties in sync with gradle.properties!
test {
	systemProperty 'user.language', 'en'
	systemProperty 'user.country', 'US'
	systemProperty 'user.timezone', 'UTC'
	systemProperty 'file.encoding', 'UTF-8'
	// Needed for AdoptOpenJDK on alpine? The problem is similar to this: https://github.com/mockito/mockito/issues/978
	jvmArgs '-XX:+StartAttachListener'
}

test {
	if ( project.findProperty( 'log-test-progress' )?.toString()?.toBoolean() ) {
		// Log a statement for each test.
		// Used in the Travis build so that Travis doesn't end up panicking because there's no output for a long time.
		testLogging {
			events "passed", "skipped", "failed"
			exceptionFormat = 'full'
		}
	}
}


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Jar

jar {
	manifest {
		attributes(
				// Basic JAR manifest attributes
				'Specification-Title': project.name,
				'Specification-Version': project.version,
				'Specification-Vendor': 'Hibernate.org',
				'Implementation-Title': project.name,
				'Implementation-Version': project.version,
				'Implementation-Vendor': 'Hibernate.org',
				'Implementation-Vendor-Id': 'org.hibernate',
				'Implementation-Url': 'https://hibernate.org/orm',

				// Java 9 module name
				'Automatic-Module-Name': project.java9ModuleName,

				// Hibernate-specific JAR manifest attributes
				'Hibernate-VersionFamily': project.ormVersion.family,
				'Hibernate-JpaVersion': project.jpaVersion.name,

				// BND Plugin instructions (for OSGi):
				'-reproducible': true,
				'-noextraheaders': true,
				'Bundle-Name': project.name,
				'Bundle-SymbolicName': project.java9ModuleName,
				'Bundle-Vendor': 'Hibernate.org',
				'Bundle-DocURL': "https://www.hibernate.org/orm/${project.ormVersion.family}",
				// This is overridden in some sub-projects
				'Import-Package': [
						// Temporarily support JTA 1.1 -- Karaf and other frameworks still
						// use it.  Without this, the plugin generates [1.2,2).
						'javax.transaction;version="[1.1,2)"',
						// Also import every package referenced in the code
						// (note that '*' is resolved at build time to a list of packages)
						'*'
				].join( ',' ),
				'-exportcontents': "*;version=${project.version}"
		)
	}
}


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// sources

task sourcesJar(type: Jar) {
	from project.sourceSets.main.allSource
	manifest {
		attributes(
				// Basic JAR manifest attributes
				'Specification-Title': project.name,
				'Specification-Version': project.version,
				'Specification-Vendor': 'Hibernate.org',
				'Implementation-Title': project.name,
				'Implementation-Version': project.version,
				'Implementation-Vendor': 'Hibernate.org',
				'Implementation-Vendor-Id': 'org.hibernate',
				'Implementation-Url': 'https://hibernate.org/orm',

				// Hibernate-specific JAR manifest attributes
				'Hibernate-VersionFamily': project.ormVersion.family,
				'Hibernate-JpaVersion': project.jpaVersion.name
		)
	}
	archiveClassifier.set( 'sources' )
}


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Javadoc

tasks.named( "javadoc", Javadoc ) {
	configure( options ) {
		windowTitle = "Hibernate Javadocs ($project.name)"
		docTitle = "Hibernate Javadocs ($project.name : $project.version)"
	}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// IDE


/*
 The latest versions of IntelliJ copy the test resources into out/test/resources and
 use those for its test classpath.  Unfortunately, this occurs before the placeholder
 in the test config file are substituted with the testing values.

 This behaviour prevents the execution of the hibernate tests from inside the IDE.

 A solution is to enable the 'After Build' Execution of the copyResourcesToIntelliJOutFolder task
 from the 'Gradle project' IntelliJ tool window ( The task can be found under hibernate-orm > Task > other)
 */
task copyResourcesToIntelliJOutFolder(type: Task, dependsOn: project.tasks.processTestResources) {
	doLast {
		copy {
			from "$buildDir/resources/test"
			into 'out/test/resources'
		}
	}
}



/*
 Use this task to set the current DB in a given module.

 > gradlew sDB -Pdb=mysql

 Afterward, you can run any test from the IDE against that particular DB.
 */
task setDataBase dependsOn( processTestResources, copyResourcesToIntelliJOutFolder ) {
	println( "Setting current database to ${db}" )
}

tasks.copyResourcesToIntelliJOutFolder.mustRunAfter processTestResources

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Report configs

tasks.register('enforceRules') {
	doLast {
		def illegalImport = ~/^import (sun|java.awt|org.slf4j)/
		def missingNewline = ~/^\s*}\s*(else|catch|finally)/
		def lowerEll = ~/\b\d+l\b/
		def equals = ~/boolean +equals\((@?\w+ )*Object \w+\)/
		def hashCode = ~/int +hashCode\(\)/
		def errors = 0
		def tree = fileTree("src/main/java/")
		tree.include "**/*.java"
		tree.each { file ->
			def lineNum = 0
			def shortName = file.path.substring(rootDir.path.length())
			def equalsMinusHashcode = 0
			file.eachLine { line ->
				lineNum++
				if (line =~ illegalImport) {
					errors++
					logger.error("Illegal import in ${shortName}\n${lineNum}: ${line}")
				}
				if (line =~ missingNewline) {
					errors++
					logger.error("Missing newline in ${shortName}\n${lineNum}: ${line}")
				}
				if (line =~ lowerEll) {
					errors++
					logger.error("Lowercase long literal in ${shortName}\n${lineNum}: ${line}")
				}
				if (!line.startsWith("//")) { //ignore commented-out code
					if (line =~ equals) {
						equalsMinusHashcode ++
					}
					if (line =~ hashCode) {
						equalsMinusHashcode --
					}
				}
			}
			if (equalsMinusHashcode>0) {
				errors++
				logger.error("Equals with missing hash code in ${shortName}")
			}
			if (equalsMinusHashcode<0) {
				errors++
				logger.error("Hash code with missing equals in ${shortName}")
			}
		}
		if ( errors>0 ) {
			throw new GradleException("Code rules were violated ($errors problems)")
		}
	}
}

spotless {
	//Don't fail during the check: rather than enforcing guidelines, we use this plugin to fix mistakes automatically.
	enforceCheck false
	java {
		targetExclude( "**/target/**/*.java" )
		licenseHeaderFile rootProject.file( 'spotless.license.java' )
		removeUnusedImports()
		indentWithTabs( 4 )
		trimTrailingWhitespace()
		endWithNewline()
	}
}

tasks.check.dependsOn enforceRules

tasks.register( "ciCheck" ) {
	// Task used by CI builds
	group "verification"
	description "Checks for CI environments"
	dependsOn tasks.check
}

gradle.taskGraph.whenReady {
	if ( it.hasTask( tasks.ciCheck ) ) {
		// These are already enforced by the formatChecks task
		tasks.spotlessApply.enabled = false
		tasks.spotlessJavaApply.enabled = false
		tasks.enforceRules.enabled = false
	}
}

tasks.register( "formatChecks" ) {
	// Only runs static code analysis, doesn't require compilation
	group "verification"
	description "Code style and formatting checks"

	dependsOn tasks.spotlessCheck
	dependsOn tasks.enforceRules
}

class CompilerStubsArgumentProvider implements CommandLineArgumentProvider {

    @InputDirectory
    @PathSensitive(PathSensitivity.NONE)
    File stubsDir

    @Override
    Iterable<String> asArguments() {
        { return ["-Astubs=${stubsDir}"]}
    }
}

tasks.withType(JavaCompile).configureEach { task ->
    // stubs argument needs to be passed as an absolute path, JavaCompile uses the Worker API which changes the current
    // working directory and prevents from using a relative path to locate a project file.
    // Using a CommandLineArgumentProvider allows build cache hits when the build cache is relocated.
    task.options.compilerArgumentProviders.add(new CompilerStubsArgumentProvider(stubsDir: new File(project.rootDir, "checkerstubs")))

	if (System.getProperty('APT_DEBUG', 'false') == 'true') {
		task.options.forkOptions.jvmArgs += ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9099']
	}
}

checkerFramework {
	excludeTests = true
	checkers = [
			'org.checkerframework.checker.nullness.NullnessChecker'
	]
	extraJavacArgs = [
            '-AsuppressWarnings=initialization',
            // stubs is passed directly through options.compilerArgumentProviders
            '-AonlyDefs=^org\\.hibernate\\.(jdbc|exception|integrator|processor|service|spi|pretty|property\\.access|stat|engine\\.(config|jndi|profile|spi|transaction)|(action|context|bytecode)\\.spi)\\.'
	]
}

tasks.forbiddenApisMain {
	// unfortunately we currently have many uses of default Locale implicitly (~370)
	// which need to be fixed before we can enable the "unsafe" check
	//bundledSignatures += ["jdk-system-out", "jdk-non-portable", "jdk-unsafe-${jdkVersions.baseline}"]
	bundledSignatures += ["jdk-system-out", "jdk-non-portable"]

	suppressAnnotations += [
			"org.hibernate.internal.build.AllowSysOut",
			"org.hibernate.internal.build.AllowPrintStacktrace",
			"org.hibernate.internal.build.AllowNonPortable"
	]
}

tasks.forbiddenApisTest {
	enabled = false
}