Ctrl K
ComunidadGuías…

build.gradle

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
buildscript {
	repositories {
		mavenCentral()
	}
	
	dependencies {
		classpath "com.monochromeroad.gradle-plugins:gradle-aws-s3-sync:0.10"
	}
}

//plugins {
//	//// tool to see tasks dependencies, use: gradlew <mytask> taskTree
//    id 'com.dorongold.task-tree' version '1.5'
//}

plugins {
  id 'io.github.ben-manes.versions' version '0.67.0'
  id 'org.owasp.dependencycheck' version '12.2.2'
}

import com.monochromeroad.gradle.plugin.aws.s3.S3Sync
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
import groovy.json.JsonSlurper
import org.apache.tools.ant.filters.ReplaceTokens
import groovy.xml.XmlSlurper
import groovy.xml.XmlUtil

apply plugin: 'eclipse'
apply plugin: 'maven-publish'

ext.nvdApiKey = findProperty('nvdApiKey') ?: System.getenv('NVD_API_KEY')

ext.convertigoVersion = '8.5.0'
ext.convertigoTag = 'beta'
ext.convertigoCodename = 'astatine'
ext.convertigoCode = 'At'
ext.convertigoRevision = hasProperty('convertigoRevision') ? convertigoRevision : ''
ext.jxBrowserGroup = 'com.teamdev.jxbrowser'
ext.jxBrowserVersion = '9.5.6'
ext.swaggerUiVersion = '5.34.1'
ext.tomcatVersion = '11.0.27'
ext.eclipseVersion = '2026-09'
ext.eclipseBase = '4.41'
ext.studioWebNodeVersion = '26'
ext.sapjco3Version = 'com.convertigo.lib:sapjco3:2026.09.17'
ext.soapuiVersion = '5.9.1'
ext.groovyVersion = '3.0.25'
ext.log4jVersion = '2.26.1'
ext.commonsIo = '2.22.0'
ext.commonsLang3 = '3.21.0'
ext.zstdJniVersion = '1.5.7-25'
ext.brotli4jVersion = '1.24.0'
ext.jacksonVersion = '2.22.4'
ext.xmlSchemaVersion = '1.4.7'

ext.convertigoMajor = convertigoVersion.replaceFirst('(\\d+\\.\\d+)\\..*', '$1')
ext.convertigoEngineAPI = "${convertigoVersion}-SNAPSHOT"

ext.convertigoVersionFull = "${convertigoVersion}${convertigoTag.isEmpty() ? '' : '-' + convertigoTag}"
ext.currentYear = '' + Calendar.getInstance().get(Calendar.YEAR)

// Dev Studio in Eclipse: the providers are used as jars, unless c8o.localProviders links their
// local checkouts as sources (see gradle.properties). -P wins over local.properties (per checkout,
// ignored by git), which wins over the gradle.properties files.
def localProperties = new Properties()
if (file('local.properties').exists()) {
	file('local.properties').withInputStream { localProperties.load(it) }
}
def devProperty = { String key ->
	def cli = gradle.startParameter.projectProperties
	cli.containsKey(key) ? cli[key] : localProperties.containsKey(key) ? localProperties.getProperty(key) : findProperty(key)
}
def eclipseProviderSources = [
	couchdb: 'src/com/twinsoft/convertigo/engine/providers/couchdb',
	sap: 'src/com/twinsoft/convertigo/engine/providers/sapjco',
	plugins: 'src/com/twinsoft/convertigo/engine/plugins'
]
def localProvidersValue = (devProperty('c8o.localProviders') ?: '').toString().trim()
def localProviderNames = localProvidersValue == 'all' ? eclipseProviderSources.keySet().toList() : localProvidersValue.tokenize(', ').unique()
def unknownLocalProviders = localProviderNames - eclipseProviderSources.keySet()
def localProvidersDirValue = (devProperty('c8o.localProvidersDir') ?: '').toString().trim()
def localProvidersDir = localProvidersDirValue ? file(localProvidersDirValue.replaceFirst('^~(?=/|$)', System.getProperty('user.home'))) : rootDir.parentFile
ext.localProviders = (localProviderNames - unknownLocalProviders).collect { name ->
	def checkout = new File(localProvidersDir, "convertigo-provider-${name}")
	[name: name, path: eclipseProviderSources[name], checkout: checkout, sources: new File(checkout, eclipseProviderSources[name])]
}
ext.localProvidersDescription = localProviders ? "local sources of ${localProviders*.name.join(', ')} from ${localProvidersDir}" : 'jars'

// checked before any Eclipse task runs, so that cleanEclipse does not remove the current files
gradle.taskGraph.whenReady { graph ->
	if (!graph.allTasks.any { it.name == 'eclipseProject' }) {
		return
	}
	if (unknownLocalProviders) {
		throw new GradleException("Unknown c8o.localProviders ${unknownLocalProviders}: use 'all' or a list of ${eclipseProviderSources.keySet().join(', ')}")
	}
	def missing = localProviders*.sources.findAll { !it.directory }
	if (missing) {
		throw new GradleException("c8o.localProviders: missing provider sources ${missing.join(', ')}; clone the providers there or set c8o.localProvidersDir")
	}
}

// the source sets exclude the plugins package, which comes from the provider: when it is linked as
// sources, Eclipse compiles it with the excludes of the provider build
ext.applyLocalProvidersToClasspath = { XmlProvider xml ->
	def plugins = localProviders.find { it.name == 'plugins' }
	if (plugins == null) {
		return
	}
	def providerBuild = new File(plugins.checkout, 'build.gradle')
	def providerExcludes = providerBuild.exists() ? (providerBuild.text =~ /exclude\s+['"](com\/twinsoft\/convertigo\/engine\/plugins\/[^'"]+)['"]/).collect { it[1] } : []
	def nl = xml.asElement().getElementsByTagName('classpathentry')
	for (def i = 0; i < nl.length; i++) {
		def entry = nl.item(i)
		if (entry.getAttribute('kind') == 'src' && entry.getAttribute('path') == 'src') {
			def patterns = entry.getAttribute('excluding').tokenize('|').findAll { it != 'com/twinsoft/convertigo/engine/plugins/**' } + providerExcludes
			if (patterns) {
				entry.setAttribute('excluding', patterns.join('|'))
			} else {
				entry.removeAttribute('excluding')
			}
		}
	}
}

apply from: 'headers.gradle'
apply from: 'changelog.gradle'
apply from: 'installers/windows.gradle'

def isDependencyUpdatesTaskRequested = gradle.startParameter.taskNames.any { taskName ->
	taskName == 'dependencyUpdates' ||
	taskName == 'dependencyUpdatesPatch' ||
	taskName.endsWith(':dependencyUpdates') ||
	taskName.endsWith(':dependencyUpdatesPatch')
}

if (isDependencyUpdatesTaskRequested) {
	gradle.startParameter.parallelProjectExecutionEnabled = false
}

task updateVersion(dependsOn: [':engine:updateProductVersion', 'writeManifest', 'generateDockerfile']) {
	group = 'convertigo'
	description = 'Run this task after a Convertigo version change.'
}

configurations.all {
	// Check for updates every build
	resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}

eclipse {
	project {
		comment = 'Main project to develop the Convertigo platform'
	}
}

allprojects {
	apply plugin: 'io.github.ben-manes.versions'
	apply plugin: 'org.owasp.dependencycheck'
	dependencyCheck {
		nvd {
			apiKey = rootProject.ext.nvdApiKey
		}
	}
	version = convertigoVersionFull
	
	repositories {
		mavenLocal()
		maven {
			url = "https://m2.convertigo.com"
		}
		maven {
			url = "https://europe-maven.pkg.dev/jxbrowser/releases"
		}
		mavenCentral()
		maven {
			url = "https://oss.sonatype.org/content/repositories/snapshots"
		}
		maven {
			url = "https://rapi.tools.ops.smartbear.io/nexus/content/groups/public"
		}
	}
	
	if (project.name.startsWith('eclipse')) {
		apply plugin: 'eclipse'
	}
	
	if (project.name.startsWith('eclipse-plugin')) {
		apply plugin: 'java'
		
		sourceSets {
			main {
				java {
					exclude '.placeholder'
					srcDirs = ['src']
				}
				resources {
					exclude '.placeholder'
					srcDirs = ['src']
				}
			}
		}
		
		eclipse {
			project {
				natures 'org.eclipse.jdt.core.javanature',
						'org.eclipse.pde.PluginNature',
						'org.eclipse.buildship.core.gradleprojectnature'
				buildCommand 'org.eclipse.pde.ManifestBuilder'
				buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
			}
			
			classpath.file.withXml {
				def elt = it.asElement()
				def projectPath = projectDir.path.replace('\\', '/') + '/'
				
				def nl = elt.getElementsByTagName('classpathentry')
				def hasRequired = false
				for (def i = 0; i < nl.length; i++) {
					def entry = nl.item(i)
					if (entry.getAttribute('kind') == 'lib') {
						entry.setAttribute('path', entry.getAttribute('path').replace(projectPath, ''))
					}
					hasRequired |= entry.getAttribute('path') == 'org.eclipse.pde.core.requiredPlugins'
				}
				if (!hasRequired) {
					def entry = elt.getOwnerDocument().createElement('classpathentry')
					entry.setAttribute('kind', 'con')
					entry.setAttribute('path', 'org.eclipse.pde.core.requiredPlugins')
					elt.appendChild(entry)
				}
			}
		}
		
		configurations {
			toSync
		}
		
		task syncLib(type: Sync) {
			group = 'convertigo'
			
			into 'lib'
			from configurations.toSync
			
			rename 'sapjco3.*jar', 'sapjco3.jar'
			
			// optional provider jar, copied by hand (see the Bundle-ClassPath of the studio plugin)
			preserve {
				include 'convertigo-provider-plugins.jar'
			}
		}
		
		// the .classpath lists the jars of lib: write it once syncLib has updated them
		tasks.named('eclipseClasspath') {
			mustRunAfter syncLib
		}
	}
	
	plugins.withId('java') {
		java {
			toolchain.languageVersion = JavaLanguageVersion.of(25)
		}
		tasks.withType(JavaCompile).configureEach {
			options.encoding = 'UTF-8'
			options.release = 25
		}
		
		task cleanBin(type: Delete) {
			group = 'convertigo'
			
			def f = project.file('bin')
			if (f.exists()) {
				f.eachFile {
					if (it.getName() != 'main') {
						println it
						delete it
					}
				}
			}
		}
	}
	
	afterEvaluate {
		if (plugins.hasPlugin('maven-publish')) {
			publishing {
				repositories {
					maven {
						url = project.properties['c8o.m2.url']
						credentials(AwsCredentials) {
							accessKey = project.properties['c8o.m2.ak']
							secretKey = project.properties['c8o.m2.sk']
						}
					}
				}
			}
		}
	}
}

configurations {
	soapuiLightBase
}

dependencies {
	soapuiLightBase("com.smartbear.soapui:soapui:${soapuiVersion}") {
		transitive = false
	}
}

tasks.register('soapuiLightJar', Jar) {
	group = 'convertigo'
	description = 'Build a SoapUI light jar without explorer UI assets.'
	destinationDirectory = layout.buildDirectory.dir('libs')
	archiveFileName = "soapui-light-${soapuiVersion}.jar"
	from {
		configurations.soapuiLightBase.files.collect { zipTree(it) }
	}
	exclude 'com/eviware/soapui/explorer/**'
}

publishing {
	publications {
		engineApi (MavenPublication) {
			groupId = 'com.convertigo.lib'
			artifactId = 'convertigo-engine-api'
			version = convertigoEngineAPI
			afterEvaluate {
				artifact project(':engine').engineApiJar
				pom.withXml {
					def dependencies = asNode().appendNode('dependencies')
					[
						'org.apache.logging.log4j:log4j-core:' + log4jVersion,
						'org.apache.logging.log4j:log4j-1.2-api:' + log4jVersion,
						'com.convertigo.twinsoft:api:2017.12.29',
						'com.convertigo.twinsoft:tas:2019.10.16',
						'com.convertigo.twinsoft:util:2017.12.29',
						'com.fasterxml.jackson.core:jackson-core:' + jacksonVersion,
						'commons-io:commons-io:' + commonsIo,
						'javax.mail:mail:1.4.7',
						'jakarta.servlet:jakarta.servlet-api:6.1.0',
						'org.apache.commons:commons-lang3:' + commonsLang3,
						'org.apache.httpcomponents:httpclient:4.5.14',
						'org.apache.ws.commons.schema:XmlSchema:' + xmlSchemaVersion,
						'org.codehaus.jettison:jettison:1.5.7'
					].each {
						def values = it.split(':')
						def dependency = dependencies.appendNode('dependency')
						dependency.appendNode('groupId', values[0])
						dependency.appendNode('artifactId', values[1])
						dependency.appendNode('version', values[2])
					}
				}
			}
		}
		soapuiLight (MavenPublication) {
			groupId = 'com.smartbear.soapui'
			artifactId = 'soapui-light'
			version = soapuiVersion
			artifact tasks.named('soapuiLightJar')
			pom {
				name = 'SoapUI light'
				description = 'SoapUI jar with jquery assets removed and no transitive dependencies.'
			}
			pom.withXml {
				def dependencies = asNode().appendNode('dependencies')
				def dependency = dependencies.appendNode('dependency')
				dependency.appendNode('groupId', 'com.btr')
				dependency.appendNode('artifactId', 'proxy-vole')
				dependency.appendNode('version', '20131209')
				dependency = dependencies.appendNode('dependency')
				dependency.appendNode('groupId', 'commons-lang')
				dependency.appendNode('artifactId', 'commons-lang')
				dependency.appendNode('version', '2.6')
				dependency = dependencies.appendNode('dependency')
				dependency.appendNode('groupId', 'org.apache.xmlbeans')
				dependency.appendNode('artifactId', 'xmlbeans')
				dependency.appendNode('version', '3.1.1-sb-fixed')
				def exclusions = dependency.appendNode('exclusions')
				def exclusion = exclusions.appendNode('exclusion')
				exclusion.appendNode('groupId', 'stax')
				exclusion.appendNode('artifactId', 'stax-api')
				dependency = dependencies.appendNode('dependency')
				dependency.appendNode('groupId', 'org.reflections')
				dependency.appendNode('artifactId', 'reflections')
				dependency.appendNode('version', '0.9.9-RC1')
				exclusions = dependency.appendNode('exclusions')
				exclusion = exclusions.appendNode('exclusion')
				exclusion.appendNode('groupId', 'dom4j')
				exclusion.appendNode('artifactId', 'dom4j')
				dependency = dependencies.appendNode('dependency')
				dependency.appendNode('groupId', 'org.codehaus.groovy')
				dependency.appendNode('artifactId', 'groovy-all')
				dependency.appendNode('version', groovyVersion)
				exclusions = dependency.appendNode('exclusions')
				exclusion = exclusions.appendNode('exclusion')
				exclusion.appendNode('groupId', 'jline')
				exclusion.appendNode('artifactId', 'jline')
				dependency = dependencies.appendNode('dependency')
				dependency.appendNode('groupId', 'com.fifesoft')
				dependency.appendNode('artifactId', 'rsyntaxtextarea')
				dependency.appendNode('version', '2.5.0')
			}
		}
/*		
		jxBrowser (MavenPublication) {
			groupId 'com.convertigo.lib'
			artifactId 'jxbrowser'
			version "${jxBrowserVersion}"
			artifact "resources/jxbrowser-${jxBrowserVersion}/jxbrowser-${jxBrowserVersion}.jar"
		}
		
		jxBrowserLinux64 (MavenPublication) {
			groupId 'com.convertigo.lib'
			artifactId 'jxbrowser-linux64'
			version "${jxBrowserVersion}"
			artifact "resources/jxbrowser-${jxBrowserVersion}/jxbrowser-linux64-${jxBrowserVersion}.jar"
		}
		
		jxBrowserMac (MavenPublication) {
			groupId 'com.convertigo.lib'
			artifactId 'jxbrowser-mac'
			version "${jxBrowserVersion}"
			artifact "resources/jxbrowser-${jxBrowserVersion}/jxbrowser-mac-${jxBrowserVersion}.jar"
		}
		
		jxBrowserWin64 (MavenPublication) {
			groupId 'com.convertigo.lib'
			artifactId 'jxbrowser-win64'
			version "${jxBrowserVersion}"
			artifact "resources/jxbrowser-${jxBrowserVersion}/jxbrowser-win64-${jxBrowserVersion}.jar"
		}
		
		jxBrowserSwt (MavenPublication) {
			groupId 'com.convertigo.lib'
			artifactId 'jxbrowser-swt'
			version "${jxBrowserVersion}"
			artifact "resources/jxbrowser-${jxBrowserVersion}/jxbrowser-swt-${jxBrowserVersion}.jar"
		}
		
		ojdbc (MavenPublication) {
			groupId 'com.convertigo.lib'
			artifactId 'ojdbc7'
			version '12.1.0.2'
			artifact 'resources/ojdbc7.jar'
		}
		
		xmlbeans (MavenPublication) {
			groupId 'com.convertigo.lib'
			artifactId 'xmlbeans'
			version '2.6.0'
			artifact 'resources/xmlbeans-2.6.0.jar'
		}
		
		rhinoTools (MavenPublication) {
			groupId 'org.mozilla'
			artifactId 'rhino-tools'
			version '1.8.0'
			artifact 'resources/rhino-tools-1.8.0.jar'
		}
*/
	}
}

afterEvaluate {
	ext.sharedManifest = project(':engine').java.manifest {
		attributes(
			'Bundle-ManifestVersion': '2',
			'Bundle-Vendor': 'Convertigo',
			'Bundle-Version': "${convertigoVersion}.qualifier",
			'Bundle-RequiredExecutionEnvironment': 'JavaSE-25',
		)
	}
}

task writeManifest(dependsOn:  ['updateEclipseVersion', 'makeSplash']) {
	group = 'convertigo'
	
	def wm = getTasksByName 'writeManifest', true
	wm.remove tasks['writeManifest']
	wm.each { it.dependsOn ':updateEclipseVersion' }
	dependsOn wm
}

task buildStudio(type: Exec, dependsOn: ['writeManifest', 'downloadSystemLibraries']) {
	group = 'convertigo'
	
	onlyIf { !file('eclipse-repository/target/products').exists() }
	
	if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
		commandLine 'cmd', '/c', 'mvn'
	} else {
		commandLine 'mvn'
	}
	
	args 'install'
}

task updateEclipseProduct(type: Copy, dependsOn: [':checkLicenseDate', 'updateEclipseCategory', ':eclipse-plugin-product:writePluginXml', ':eclipse-plugin-product:writeAboutIni']) {
	group = 'convertigo'
	
	def aboutTextFile = file('eclipse-plugin-product/aboutText.txt')
	def licenseTextFile = file('legal/dual-license.txt')
	
	inputs.properties(
		'currentYear': currentYear,
		'convertigoVersionFull': convertigoVersionFull
	)
	inputs.files aboutTextFile, licenseTextFile
	
	def aboutText = ''
	def licenseText = ''
	
	doFirst {
		def xml = new XmlSlurper().parseText('<elt/>')
		xml.replaceBody aboutTextFile.text
			.replace('@CEMS_VERSION@', convertigoVersionFull)
			.replace('@CURRENT_YEAR@', currentYear)
		aboutText = XmlUtil.serialize(xml).replaceFirst('[\\d\\D]*<elt>([\\d\\D]*)</elt>[\\d\\D]*','$1')
		
		xml.replaceBody licenseTextFile.text
		licenseText = XmlUtil.serialize(xml).replaceFirst('[\\d\\D]*<elt>([\\d\\D]*)</elt>[\\d\\D]*','$1')
	}
	
	from 'eclipse-repository/Convertigo.product.tpl'
	filter {
		it.replace('@aboutText@', aboutText)
			.replace('@licenseText@', licenseText)
			.replace('@convertigoVersion@', convertigoVersion)
			.replace('@convertigoMajor@', convertigoMajor)
			.replace('@convertigoMode@', convertigoTag.isEmpty() ? 'release' : 'beta')
			.replace('@eclipseVersion@', eclipseVersion)
	}
	into 'eclipse-repository'
	rename '.*', 'Convertigo.product'
}

task updateEclipseCategory(type: Copy) {
	group = 'convertigo'
	
	inputs.properties(
		'convertigoVersion': convertigoVersion
	)
	
	from 'eclipse-repository/category.xml.tpl'
	filter { it.replace('@convertigoVersion@', convertigoVersion) }
	into 'eclipse-repository'
	rename '.*', 'category.xml'
}

task updateEclipseVersion(dependsOn: [':updateEclipseProduct', ':eclipse-plugin-product:writeAboutIni', ':eclipse-plugin-product:writePluginXml', ':eclipse-feature:updateFeature']) {
	group = 'convertigo'
	doFirst {
		fileTree(dir: '.', include: '*/pom.xml').files.plus(file('pom.xml')).each {
			it.text = it.text.replaceAll('(<!-- SET-VERSION -->\\s*<version>).*(</version>)', "\$1${convertigoVersion}-SNAPSHOT\$2")
		}
		def base = file('eclipse-base/base.target')
		base.text = base.text.replaceFirst('/releases/.+?/', "/releases/${eclipseVersion}/").replaceFirst('/updates/.+?/', "/updates/${eclipseBase}/")
	}
}

task buildStudioClean(type: Delete) {
	group = 'convertigo'
	
	delete 'eclipse-repository/target/products'
}

task generateEclipseConfiguration(dependsOn: [getTasksByName('cleanEclipse', true), getTasksByName('eclipse', true), getTasksByName('cleanBin', true)]) {
	group = 'convertigo'
}

task generateEclipseConfigurationWithManifest(dependsOn: ['writeManifest', 'generateEclipseConfiguration']) {
	group = 'convertigo'
	finalizedBy 'checkEclipseConfiguration'
}

task checkEclipseConfiguration {
	group = 'convertigo'
	description = 'Checks that the Eclipse .classpath and .project files only reference existing files.'
	
	doLast {
		def errors = []
		allprojects.findAll { it.name == 'engine' || it.name.startsWith('eclipse-plugin') }.each { p ->
			def classpathFile = p.file('.classpath')
			if (classpathFile.exists()) {
				def libs = new XmlSlurper().parse(classpathFile).classpathentry.findAll { it.@kind == 'lib' }.collect { it.@path.text() }
				libs.findAll { !(new File(it).absolute ? new File(it) : p.file(it)).exists() }.each {
					errors << "${p.name}/.classpath: missing library ${it}"
				}
				// projects whose .classpath mirrors lib (implementation fileTree(dir: 'lib'))
				if (p.configurations.findByName('implementation')?.dependencies?.any { it instanceof FileCollectionDependency }) {
					p.fileTree(dir: 'lib', include: '*.jar').each {
						if (!libs.contains('lib/' + it.name)) {
							errors << "${p.name}/.classpath: lib/${it.name} is not listed"
						}
					}
				}
			}
			def projectFile = p.file('.project')
			if (projectFile.exists()) {
				new XmlSlurper().parse(projectFile).linkedResources.link.each { link ->
					def location = link.location.text()
					def uri = link.locationURI.text()
					def target = location ? new File(location) : null
					def parent = uri =~ /^PARENT-(\d+)-PROJECT_LOC\/(.*)$/
					if (parent.matches()) {
						target = p.projectDir
						parent.group(1).toInteger().times { target = target.parentFile }
						target = new File(target, parent.group(2))
					} else if (uri.startsWith('file:')) {
						target = new File(new URI(uri))
					}
					if (target != null && !target.exists()) {
						errors << "${p.name}/.project: missing linked resource ${link.name.text()} -> ${target}"
					}
				}
			}
		}
		if (errors) {
			throw new GradleException("Eclipse configuration is inconsistent:\n  " + errors.join('\n  '))
		}
		logger.lifecycle "Eclipse configuration checked, providers: ${localProvidersDescription}"
	}
}

task setupStudioDev(dependsOn: ['generateEclipseConfigurationWithManifest', 'buildStudioWebNpmInstall', 'buildStudioWebBuild']) {
	group = 'convertigo'
	description = 'Prepares the dev Studio: Eclipse configuration, manifests and admin web build.'
}

task makeSplash(dependsOn: 'convertigo-splash:run') {
	group = 'convertigo'
}

// the admin web builds with the Node of nvm on macOS and Linux, and with the Node of the PATH on Windows
def studioWebOnWindows = System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')
def studioWebCommand = { String command ->
	studioWebOnWindows ? ['cmd', '/c', command] : ['sh', '-c', ". ~/.nvm/nvm.sh && nvm exec ${studioWebNodeVersion} ${command}"]
}

task buildStudioWebNvmInstall(type: Exec) {
	group = 'convertigo'
	onlyIf { !studioWebOnWindows }
	
	workingDir 'convertigo-studio-web'
	commandLine 'sh'
	args '-c', ". ~/.nvm/nvm.sh && nvm install ${studioWebNodeVersion}"
}

task buildStudioWebNpmInstall(type: Exec, dependsOn: ':buildStudioWebNvmInstall') {
	group = 'convertigo'
	
	// up-to-date while the lock file is unchanged and node_modules holds its installation
	inputs.property('studioWebNodeVersion', studioWebNodeVersion)
	inputs.files('convertigo-studio-web/package.json', 'convertigo-studio-web/package-lock.json')
	outputs.file('convertigo-studio-web/node_modules/.package-lock.json')
	
	workingDir 'convertigo-studio-web'
	commandLine studioWebCommand('npm ci')
}

task buildStudioWebBuild(type: Exec, dependsOn: ':buildStudioWebNvmInstall') {
	group = 'convertigo'
	mustRunAfter ':buildStudioWebNpmInstall'
	
	// up-to-date while the admin web sources are unchanged and its output is in the studio webapp
	// (post-build.js copies it among other files, so a stamp stands for it)
	def webapp = file('eclipse-plugin-studio/tomcat/webapps/convertigo')
	def stamp = layout.buildDirectory.file('tmp/buildStudioWebBuild/stamp')
	inputs.property('studioWebNodeVersion', studioWebNodeVersion)
	inputs.files(fileTree('convertigo-studio-web') {
		// generated or local files, see convertigo-studio-web/.gitignore
		exclude 'node_modules/', 'build/', '.svelte-kit/', '.cache/', 'package/', '.vite/', 'static/monaco/'
		exclude 'src/convertigo.utilities.css', 'assets/bezel-sources/', 'vite.config.*.timestamp-*', '.env*', '**/.DS_Store'
		exclude 'tests/', 'test-results/', 'playwright-report/'
	}).withPropertyName('studioWebSources')
	outputs.file(stamp)
	outputs.upToDateWhen { ['index.html', '_app', 'admin'].every { new File(webapp, it).exists() } }
	doLast {
		stamp.get().asFile.text = 'built\n'
	}
	
	workingDir 'convertigo-studio-web'
	commandLine studioWebCommand('npm run build')
}

task ciFirstStep(dependsOn: [':engine:jar', ':gradle-plugin:publish', ':buildStudioWebNpmInstall', ':buildStudioWebBuild']) {
	group = 'convertigo'
}

task generateDockerfile {
	group = 'convertigo'
	
	doLast {
		def base = file('docker/Dockerfile.tpl').text
		base = base.replace('%VERSION%', convertigoVersion)
		def generatedDockerfileHeader = '# Generated by ./gradlew generateDockerfile. Do not edit directly.\n'
		def generatedScriptHeader = '# Generated by ./gradlew generateDockerfile from docker/docker-entrypoint.sh. Do not edit directly.\n'
		def sourceDockerfileHeader = '# Source file for docker/*/Dockerfile, regenerated with ./gradlew generateDockerfile.\n'
		def sourceScriptHeader = '# Source file for docker/*/docker-entrypoint.sh, regenerated with ./gradlew generateDockerfile.\n'
		def generatedBase = base.replace(sourceDockerfileHeader, '')
		def generatedEntryPoint = { String onLaunch ->
			file('docker/docker-entrypoint.sh').text
				.replace('%ON_LAUNCH%', onLaunch)
				.replace(sourceScriptHeader, '')
				.replaceFirst('^#!/bin/sh\\n', '#!/bin/sh\n' + generatedScriptHeader)
		}
		
		file('docker/default/Dockerfile').text = generatedDockerfileHeader + generatedBase.replace('%BEGIN%', '')
		file('docker/default/docker-entrypoint.sh').text = generatedEntryPoint('')
		file('docker/default/root-index.html').text = file('docker/root-index.html').text
		
		file('docker/aks/Dockerfile').text = generatedDockerfileHeader + generatedBase.replace('%BEGIN%', file('docker/aks-Dockerfile.tpl').text)
		file('docker/aks/docker-entrypoint.sh').text = generatedEntryPoint(file('docker/aks-entrypoint.tpl').text)
		file('docker/aks/root-index.html').text = file('docker/root-index.html').text
	}
}

def dockerDocsOfficialDir = {
	file(findProperty('dockerDocsDir') ?: '../docker-docs')
}

def runDockerDocsOfficialCommand = { File workingDir, List<String> command, Map<String, String> environment = [:] ->
	def normalizedCommand = command.collect { it.toString() }
	println "\$ ${normalizedCommand.collect { it.contains(' ') ? '"' + it + '"' : it }.join(' ')}"
	def processBuilder = new ProcessBuilder(normalizedCommand)
		.directory(workingDir)
		.redirectErrorStream(true)
	processBuilder.environment().putAll(environment)
	def process = processBuilder.start()
	def output = process.inputStream.getText('UTF-8')
	def exitCode = process.waitFor()
	if (output) {
		print output
	}
	if (exitCode != 0) {
		throw new GradleException("Command failed with exit code ${exitCode}: ${command.join(' ')}")
	}
	return output
}

def renderDockerDocsOfficialContent = {
	def content = []
	def inCodeBlock = false
	def dockerRunCodeBlock = false

	def stripCodeIndent = { String line ->
		return line.startsWith('\t') ? line.substring(1) : line.startsWith('    ') ? line.substring(4) : line.substring(3)
	}

	def normalizeCodeLine = { String line, boolean canReplaceImage ->
		def code = stripCodeIndent(line)
		if (code == 'FROM convertigo') {
			code = 'FROM %%IMAGE%%'
		} else if (canReplaceImage || code.trim().replaceFirst(/^\$ /, '').startsWith('docker run ')) {
			code = code.replaceFirst(/(^|\s)convertigo(\s*\\?)$/, '$1%%IMAGE%%$2')
		}
		return code
	}

	// Docker docs convention: in a console block every command starts with "$ " so the highlighter
	// tells commands from their output. The README stays plain markdown; the prompt is added here,
	// only in blocks that start with a shell command (not Dockerfile snippets nor option lists).
	def shellCodeBlock = false
	def continuedCommand = false
	def isShellCommand = { String code ->
		return code ==~ /^(\$ |[a-z][\w.-]*( |$)|[A-Za-z_][A-Za-z0-9_]*=).*/
	}

	file('docker/README.md').readLines('UTF-8').each { String line ->
		if (line ==~ /^( {3,}|\t).*/) {
			if (!inCodeBlock) {
				content << '```console'
				inCodeBlock = true
				dockerRunCodeBlock = stripCodeIndent(line).trim().replaceFirst(/^\$ /, '').startsWith('docker run ')
				shellCodeBlock = isShellCommand(stripCodeIndent(line))
				continuedCommand = false
			}
			def code = normalizeCodeLine(line, dockerRunCodeBlock)
			if (shellCodeBlock && !continuedCommand && !code.startsWith('$ ') && !code.trim().isEmpty() && !(code ==~ /^\s.*/)) {
				code = '$ ' + code
			}
			continuedCommand = code.trim().endsWith('\\')
			content << code
		} else {
			if (inCodeBlock) {
				content << '```'
				inCodeBlock = false
				dockerRunCodeBlock = false
			}
			// markdownfmt (Docker docs CI) expects list items as "-<tab>item"
			content << line.replaceFirst(/^(\s*)[-*]\s+(?=\S)/, '$1-\t')
			if (line == '> [www.convertigo.com](https://www.convertigo.com)') {
				content << ''
				content << '%%LOGO%%'
			}
		}
	}
	if (inCodeBlock) {
		content << '```'
	}
	return content.collect { it.replaceFirst(/[ \t]+$/, '') }.join('\n') + '\n'
}

task generateDockerDocsOfficialContent {
	group = 'convertigo'
	description = 'Generates the Docker Official Images documentation content from docker/README.md.'

	def output = layout.buildDirectory.file('docker-docs/convertigo/content.md')
	inputs.file 'docker/README.md'
	outputs.file output
	outputs.upToDateWhen { false }

	doLast {
		def outputFile = output.get().asFile
		outputFile.parentFile.mkdirs()
		outputFile.setText(renderDockerDocsOfficialContent(), 'UTF-8')
	}
}

task checkDockerDocsOfficial(dependsOn: 'generateDockerDocsOfficialContent') {
	group = 'convertigo'
	description = 'Checks that the sibling docker-docs repository is synchronized with docker/README.md.'

	doLast {
		def target = new File(dockerDocsOfficialDir(), 'convertigo/content.md')
		if (!target.isFile()) {
			throw new GradleException("Docker docs target not found: ${target}. Use -PdockerDocsDir=path/to/docker-docs if repositories are not siblings.")
		}
		def generated = layout.buildDirectory.file('docker-docs/convertigo/content.md').get().asFile
		if (target.getText('UTF-8') != generated.getText('UTF-8')) {
			throw new GradleException("Docker official docs are not synchronized. Run ./gradlew updateDockerDocsOfficial or inspect ${generated}.")
		}
	}
}

task updateDockerDocsOfficial(dependsOn: 'generateDockerDocsOfficialContent') {
	group = 'convertigo'
	description = 'Updates ../docker-docs/convertigo/content.md from docker/README.md. Override with -PdockerDocsDir=path.'

	doLast {
		def docsDir = dockerDocsOfficialDir()
		def target = new File(docsDir, 'convertigo/content.md')
		if (!new File(docsDir, '.git').directory) {
			throw new GradleException("Docker docs repository not found: ${docsDir}. Use -PdockerDocsDir=path/to/docker-docs if repositories are not siblings.")
		}
		if (!target.isFile()) {
			throw new GradleException("Docker docs target not found: ${target}.")
		}

		def generated = layout.buildDirectory.file('docker-docs/convertigo/content.md').get().asFile
		def generatedText = generated.getText('UTF-8')
		if (target.getText('UTF-8') != generatedText) {
			def statusProcess = new ProcessBuilder('git', 'status', '--porcelain', '--', 'convertigo/content.md')
				.directory(docsDir)
				.redirectErrorStream(true)
				.start()
			def statusOutput = statusProcess.inputStream.getText('UTF-8')
			def statusExitCode = statusProcess.waitFor()
			if (statusExitCode != 0) {
				throw new GradleException("Unable to inspect Docker docs git status in ${docsDir}: ${statusOutput.trim()}")
			}
			if (!statusOutput.trim().isEmpty() && !project.hasProperty('dockerDocsOverwrite')) {
				throw new GradleException("Refusing to overwrite locally modified Docker docs file: ${target}. Use -PdockerDocsOverwrite=true to regenerate it anyway.")
			}
			target.setText(generatedText, 'UTF-8')
		}
	}
}

task validateDockerDocsOfficial(dependsOn: 'checkDockerDocsOfficial') {
	group = 'convertigo'
	description = 'Runs Docker official docs validations for the sibling docker-docs repository.'

	doLast {
		def docsDir = dockerDocsOfficialDir()
		if (!new File(docsDir, '.git').directory) {
			throw new GradleException("Docker docs repository not found: ${docsDir}. Use -PdockerDocsDir=path/to/docker-docs if repositories are not siblings.")
		}

		runDockerDocsOfficialCommand(docsDir, ['/usr/bin/env', 'bash', '-c', '''
			files="$(
				find . \\( -name '*.md' -not -name 'README.md' \\) -exec test -s '{}' ';' -print0 \
					| xargs -0 ./markdownfmt.sh -l
			)"
			if [ -n "$files" ]; then
				echo >&2 'Need markdownfmt:'
				echo >&2 "$files"
				echo >&2
				echo "$files" | xargs ./markdownfmt.sh -d >&2
				exit 1
			fi
		'''.stripIndent().trim()])

		runDockerDocsOfficialCommand(docsDir, ['/usr/bin/env', 'bash', '-c', './.ci/check-required-files.sh && ./.ci/check-short.sh && ./.ci/check-pr-no-readme.sh'])

		runDockerDocsOfficialCommand(docsDir, ['/usr/bin/env', 'bash', '-c', '''
			needdiff=
			need=()
			while IFS= read -r f; do
				fdiff="$(./ymlfmt.sh -d "$f")"
				if [ -n "$fdiff" ]; then
					need+=( "$f" )
					needdiff+="$fdiff"$'\\n'
				fi
			done < <(find . \\( -name '*.yml' -o -name '*.yaml' \\) -not -path './.github/*')
			if [ -n "$needdiff" ]; then
				echo >&2 'Need ymlfmt:'
				printf '%s\\n' "${need[@]}" >&2
				echo >&2
				printf '%s' "$needdiff" >&2
				exit 1
			fi
		'''.stripIndent().trim()], ['DOCKER_DEFAULT_PLATFORM': findProperty('dockerDocsYmlPlatform') ?: 'linux/amd64'])

		runDockerDocsOfficialCommand(docsDir, ['docker', 'run', '--rm',
			'-v', "${docsDir.absolutePath}:/src:ro",
			'debian:trixie-slim',
			'bash', '-c', '''
				set -Eeuo pipefail
				export DEBIAN_FRONTEND=noninteractive
				apt-get update >/dev/null
				apt-get install -y --no-install-recommends ca-certificates curl git jq util-linux coreutils diffutils findutils >/dev/null

				arch="$(dpkg --print-architecture)"
				case "$arch" in
					amd64) bashbrew_asset='bashbrew-amd64' ;;
					arm64) bashbrew_asset='bashbrew-arm64v8' ;;
					*) echo >&2 "Unsupported bashbrew validation architecture: $arch"; exit 1 ;;
				esac
				curl -fsSL -o /usr/local/bin/bashbrew "https://github.com/docker-library/bashbrew/releases/download/v0.1.14/$bashbrew_asset"
				chmod +x /usr/local/bin/bashbrew

				mkdir /work
				cp -a /src/. /work/
				cd /work
				.ci/check-metadata.sh
				if ! ./update.sh convertigo >/tmp/update.log 2>&1; then
					cat /tmp/update.log >&2
					exit 1
				fi
				tail -80 /tmp/update.log
			'''.stripIndent().trim()
		])
	}
}
task downloadSystemLibFlowViewer(type: Sync) {
	group = 'convertigo'
	
	def car = file 'lib_FlowViewer.car'
	ext.dir = file 'eclipse-plugin-studio/tomcat/webapps/convertigo/system/projects/'
	
	from 'eclipse-plugin-studio/src'
	into ext.dir
		
	doFirst {
		new URL('https://github.com/convertigo/c8oprj-lib-flowviewer/releases/download/ci/lib_FlowViewer.car')
			.withInputStream{ i -> car.withOutputStream{ it << i }}
		from zipTree(car)
		exclude 'com/**'
	}
	
	doLast {
		car.delete()
	}
}

task downloadSystemLibraries(/*dependsOn: downloadSystemLibFlowViewer*/) {
	group = 'convertigo'
}

task checkLicenseDate {
	group = 'convertigo'
	
	def licenseTextFile = file('legal/dual-license.txt')
	
	inputs.properties(
		'currentYear': currentYear
	)
	inputs.files licenseTextFile
	outputs.files licenseTextFile
	
	doFirst {
		licenseTextFile.text = licenseTextFile.text.replaceAll('-20\\d\\d-', "-${currentYear}-")
	}
}

tasks.register('checkStudioJdkUpToDate') {
	group = 'convertigo'
	description = 'Checks whether installers/nsis/download-jdk.sh uses the latest Temurin LTS patch release.'

	inputs.file 'installers/nsis/download-jdk.sh'

	doLast {
		def failOnOutdated = project.findProperty('failOnOutdatedStudioJdk')?.toString()?.toBoolean() ?: false
		def script = file('installers/nsis/download-jdk.sh').text
		def releaseMatcher = (script =~ /(?m)^JDK_RELEASE=(.+)$/)
		if (!releaseMatcher.find()) {
			def msg = "Cannot read JDK_RELEASE from installers/nsis/download-jdk.sh"
			if (failOnOutdated) {
				throw new GradleException(msg)
			}
			logger.warn(msg)
			return
		}

		def configuredRelease = releaseMatcher.group(1).trim()
		def majorMatcher = (configuredRelease =~ /^(\d+)/)
		if (!majorMatcher.find()) {
			def msg = "Cannot extract JDK major from JDK_RELEASE=${configuredRelease}"
			if (failOnOutdated) {
				throw new GradleException(msg)
			}
			logger.warn(msg)
			return
		}

		def configuredMajor = majorMatcher.group(1)
		try {
			def ltsInfo = new JsonSlurper().parse(new URL("https://api.adoptium.net/v3/info/available_releases"))
			def latestLts = ltsInfo?.most_recent_lts?.toString()
			if (latestLts && latestLts != configuredMajor) {
				logger.warn("A newer Temurin LTS major is available (${latestLts}); script is pinned to ${configuredMajor}.")
			}

			def assetsUrl = "https://api.adoptium.net/v3/assets/latest/${configuredMajor}/hotspot?image_type=jdk&os=windows&architecture=x64"
			def assets = new JsonSlurper().parse(new URL(assetsUrl))
			if (!(assets instanceof List) || assets.isEmpty()) {
				def msg = "Cannot query latest Temurin release for JDK ${configuredMajor}"
				if (failOnOutdated) {
					throw new GradleException(msg)
				}
				logger.warn(msg)
				return
			}

			def latestReleaseName = assets[0]?.release_name?.toString()
			if (!latestReleaseName?.startsWith('jdk-')) {
				def msg = "Unexpected Adoptium response: release_name=${latestReleaseName}"
				if (failOnOutdated) {
					throw new GradleException(msg)
				}
				logger.warn(msg)
				return
			}

			def latestRelease = latestReleaseName.substring(4)
			if (configuredRelease == latestRelease) {
				logger.lifecycle("Studio JDK is up to date (${configuredRelease}).")
			} else {
				def msg = "Studio JDK is outdated: configured ${configuredRelease}, latest Temurin ${configuredMajor} is ${latestRelease}."
				if (failOnOutdated) {
					throw new GradleException(msg)
				}
				logger.warn(msg)
			}
		} catch (Exception e) {
			def msg = "Failed to check Studio JDK update status: ${e.message}"
			if (failOnOutdated) {
				throw new GradleException(msg, e)
			}
			logger.warn(msg)
		}
	}
}

tasks.register('checkSwaggerUiUpToDate') {
	group = 'convertigo'
	description = 'Checks whether swaggerUiVersion matches the latest swagger-ui GitHub release.'

	inputs.property('swaggerUiVersion', swaggerUiVersion)

	doLast {
		def failOnOutdated = project.findProperty('failOnOutdatedSwaggerUi')?.toString()?.toBoolean() ?: false
		def configured = swaggerUiVersion.toString()
		try {
			def connection = new URL('https://api.github.com/repos/swagger-api/swagger-ui/releases/latest').openConnection()
			connection.setRequestProperty('Accept', 'application/vnd.github+json')
			connection.setRequestProperty('User-Agent', 'convertigo-gradle')
			def release = new JsonSlurper().parse(connection.inputStream)
			def latest = release?.tag_name?.toString()?.replaceFirst(/^v/, '')
			if (!latest) {
				def msg = 'Cannot read the latest swagger-ui release tag from GitHub'
				if (failOnOutdated) {
					throw new GradleException(msg)
				}
				logger.warn(msg)
				return
			}

			if (configured == latest) {
				logger.lifecycle("swagger-ui is up to date (${configured}).")
			} else {
				def configuredLine = configured.tokenize('.').take(2).join('.')
				def latestLine = latest.tokenize('.').take(2).join('.')
				def scope = configuredLine == latestLine ? 'patch' : 'minor/major'
				def msg = "swagger-ui is outdated (${scope} update): configured ${configured}, latest release is ${latest}. Update swaggerUiVersion in build.gradle and run :engine:unzipSwaggerUI."
				if (failOnOutdated) {
					throw new GradleException(msg)
				}
				logger.warn(msg)
			}
		} catch (Exception e) {
			def msg = "Failed to check swagger-ui update status: ${e.message}"
			if (failOnOutdated) {
				throw new GradleException(msg, e)
			}
			logger.warn(msg)
		}
	}
}

def extractVersionLine = { String version ->
	def components = ((version ?: '') =~ /\d+/).collect { it.toString() }
	if (components.isEmpty()) {
		return null
	}
	while (components.size() < 3) {
		components.add('0')
	}
	return components.take(3)
}

def shouldRejectDependencyUpdate = { selection, boolean samePatchLineOnly ->
	def version = selection.candidate.version ?: ''
	def lcVersion = version.toLowerCase()
	def module = "${selection.candidate.group}:${selection.candidate.module}"

	// Reject all pre-releases consistently (case-insensitive).
	if (lcVersion.matches('.*(alpha|beta|rc|cr|m[0-9]+|ea|preview|snapshot|-b).*')) {
		return true
	}

	// Keep legacy major lines for risky dependencies.
	switch (module) {
		case 'commons-collections:commons-collections':
			return !version.startsWith('3.')
		case 'net.sf.jt400:jt400':
			return !version.startsWith('11.')
		case 'org.apache.axis2:axis2-saaj':
			return !version.startsWith('1.8.')
		case 'org.apache.xmlbeans:xmlbeans':
			// Internal patched flavor (e.g. 3.1.1-sb-fixed), keep only the exact current version.
			return version != selection.currentVersion
		case 'com.convertigo.lib:qrcode':
			// Internal Jakarta build, do not report the upstream non-Jakarta artifact as an update.
			return !version.endsWith('-jakarta')
		case 'org.apache.ws.commons.axiom:axiom-api':
		case 'org.apache.ws.commons.axiom:axiom-dom':
		case 'org.apache.ws.commons.axiom:axiom-impl':
			return !version.startsWith('1.')
	}

	// Tomcat is constrained to major 9.x.
	if (selection.candidate.group in ['org.apache.tomcat', 'org.apache.tomcat.embed']) {
		def candidateLine = extractVersionLine(version)
		def candidateMajor = candidateLine == null ? null : new BigInteger(candidateLine[0])
		if (candidateMajor != null && candidateMajor > BigInteger.valueOf(11)) {
			return true
		}
	}

	if (samePatchLineOnly) {
		def currentLine = extractVersionLine(selection.currentVersion)
		def candidateLine = extractVersionLine(version)
		if (currentLine != null && candidateLine != null && currentLine.take(2) != candidateLine.take(2)) {
			return true
		}
	}

	return false
}

def configureDependencyUpdatesReport = { DependencyUpdatesTask task, boolean samePatchLineOnly ->
	if (task.project == rootProject) {
		task.finalizedBy(rootProject.tasks.named("checkStudioJdkUpToDate"))
		task.finalizedBy(rootProject.tasks.named("checkSwaggerUiUpToDate"))
	}
	task.revision = 'release'
	task.gradleReleaseChannel = 'current'
	task.rejectVersionIf {
		shouldRejectDependencyUpdate(it, samePatchLineOnly)
	}
}

// https://github.com/ben-manes/gradle-versions-plugin
allprojects {
	tasks.named("dependencyUpdates", DependencyUpdatesTask).configure { task ->
		configureDependencyUpdatesReport(task, false)
	}

	tasks.register("dependencyUpdatesPatch", DependencyUpdatesTask) { task ->
		group = "help"
		description = "Displays dependency updates limited to the current patch line."
		configureDependencyUpdatesReport(task, true)
	}
}


['dependencyUpdates', 'dependencyUpdatesPatch'].each { taskName ->
	tasks.named(taskName, DependencyUpdatesTask).configure { task ->
		task.dependsOn(subprojects.collect { it.tasks.named(taskName) })
	}
}

wrapper {
	group = 'convertigo'
	
	gradleVersion = '9.8.2'
}