diff --git a/src/alclabs-gradle.gradle b/src/alclabs-gradle.gradle index 690beb2..644283d 100644 --- a/src/alclabs-gradle.gradle +++ b/src/alclabs-gradle.gradle @@ -7,285 +7,333 @@ apply plugin: 'war' apply plugin: 'groovy' apply plugin: 'idea' apply plugin: 'eclipse' +apply plugin:'base' -repositories { - mavenCentral() - - // since JQuery & JQueryUI are frequently used by add-ons, include their repo here as a convenience - ivy { - url "http://code.jquery.com" - layout "pattern", { - artifact "[module]-[revision](.[classifier]).[ext]" - } - } - ivy { - url "http://code.jquery.com/ui" - layout "pattern", { - artifact "[revision]/[module](.[classifier]).[ext]" - } - } +boolean includeJquery() +{ + return !project.ext.has( "excludeJquery" ) || + !"true".equals( project.ext.excludeJquery ) +} + +if( includeJquery() ) +{ + repositories { + mavenCentral() + + // since JQuery & JQueryUI are frequently used by add-ons, include their repo here as a convenience + //ivy { + // url "http://code.jquery.com" + // layout "pattern", { + // artifact "[module]-[revision](.[classifier]).[ext]" + //} + //} + //ivy { + // url "http://code.jquery.com/ui" + // layout "pattern", { + // artifact "[revision]/[module](.[classifier]).[ext]" + //} + // } + } } final jsDir = 'js' configurations { - js // used to specify javascript dependencies. These are all put in 'jsDir' when the .war is built. + js // used to specify javascript dependencies. These are all put in 'jsDir' when the .war is built. } project.convention.plugins.addoninfo = new AddOnInfo() +project.convention.plugins.addoninfo.addExtension('providers', new ProviderExtension()) -final addonInfoFile = new File(buildDir, 'tmp/war/META-INF/addon-info.xml') +final addonInfoFile = new File(buildDir, 'tmp/war/info.xml') task generateAddOnInfo { - afterEvaluate { - inputs.properties project.convention.plugins.addoninfo.properties - outputs.file addonInfoFile - } - doLast { - def addoninfo = project.convention.plugins.addoninfo - def props = addoninfo.properties - if (props.any { key, value -> value != null }) { - def text = '\r\n' - props.each { key, value -> - if (value) { - def keyText = addoninfo.propNameToXmlElementName(key) - text += " <$keyText>$value\r\n" - } - } - text += '' - - addonInfoFile.parentFile.mkdirs() - addonInfoFile.text = text - } - else - addonInfoFile.delete() - } + afterEvaluate { + inputs.properties project.convention.plugins.addoninfo.properties + outputs.file addonInfoFile + } + doLast { + def text = new StringWriter() + new groovy.xml.MarkupBuilder(text).extension(version: 1) { + project.convention.plugins.addoninfo.addXml(delegate) + } + + addonInfoFile.parentFile.mkdirs() + addonInfoFile.text = text.toString() + } +} + +task addon(description: "Produces the .addon file", type: Zip) { + dependsOn generateAddOnInfo, war + inputs.files addonInfoFile, 'LICENSE.txt' + + destinationDir = file("$buildDir/libs") + baseName = project.name.toLowerCase().replace( " ", "_" ) // lower-case'ify the addon name to simplify usage in a web browser (which is case-sensitive) + extension = 'addon' + fileMode = 0644 + + from(addonInfoFile) // include addon-info.xml in binary + from('LICENSE.txt') // include license in binary + + into ('webapp') { + with(war) + } + + // have the build tell you where the archive was put! + doLast { + println "Addon created as ${addon.archivePath}" + } } +build.dependsOn addon + war { - dependsOn generateAddOnInfo - inputs.files addonInfoFile, 'LICENSE.txt' - - // lower-case'ify the war name to simplify usage in a web browser (which is case-sensitive) - archiveName = war.baseName.toLowerCase() + '.war' - - // include addon-info.xml in binary - from(addonInfoFile) { - into 'META-INF' - } - - // include license in binary - from('LICENSE.txt') { - into 'WEB-INF' - } - - // include javascript libraries - from(configurations.js) { - into jsDir - } + enabled = false + fileMode = 0644 + + // include javascript libraries + from (configurations.js) { + into jsDir + } } // not intended to be used directly. This is the default task for the script. This allows // the user to "double-click" on 'gradlew' and have the GUI launch. -task showGui(description: 'Starts a graphical user interface for the build') << { - org.gradle.gradleplugin.userinterface.swing.standalone.BlockingApplication.launchAndBlock(); +task showGui(description: 'Starts a graphical user interface for the build') { doLast { + org.gradle.gradleplugin.userinterface.swing.standalone.BlockingApplication.launchAndBlock(); +} } // Helper method to get the installation root directory of the server. This method will either // return whatever is in the 'serverRootDir' property (if it's set) ... -File getServerDir() { - if (!project.ext.has("serverRootDir")) - project.ext.set("serverRootDir", null); - if (project.ext.serverRootDir == null) { - Properties props = new Properties() - File optionsFile = new File('build.options') - if (optionsFile.exists()) { - optionsFile.withReader { props.load(it) } - project.ext.serverRootDir = props.getProperty('serverRootDir') - } - - if (project.ext.serverRootDir == null) { - File dir = pickServerDir() - if (dir == null) - throw new Exception("""serverRootDir property (path to webctrl install directory) not set. +File determineServerDir() { + if (!project.ext.has("serverRootDir")) + project.ext.set("serverRootDir", null); + if (project.ext.serverRootDir == null) { + Properties props = new Properties() + File optionsFile = new File('build.options') + if (optionsFile.exists()) { + optionsFile.withReader { props.load(it) } + project.ext.serverRootDir = props.getProperty('serverRootDir') + } + + if (project.ext.serverRootDir == null) { + File dir = pickServerDir() + if (dir == null) + throw new Exception("""serverRootDir property (path to webctrl install directory) not set. Please set the serverRootDir property on the command line (-PserverRootDir=) or in the userHome/.gradle/gradle.properties file.""") - project.ext.serverRootDir = dir.absolutePath.replaceAll('\\\\', '/') - props.setProperty('serverRootDir', project.ext.serverRootDir) - optionsFile.withWriter { props.store(it, null) } - } - } - return new File(project.ext.serverRootDir) + project.ext.serverRootDir = dir.absolutePath.replaceAll('\\\\', '/') + props.setProperty('serverRootDir', project.ext.serverRootDir) + optionsFile.withWriter { props.store(it, null) } + } + } + return new File(project.ext.serverRootDir) } -project.ext.set("getServerDir", {getServerDir()}); +project.ext.set("getServerDir", {determineServerDir()}); // Helper methods to get the directory in which to deploy webapps (add-ons). -File getDeployLoc() { new File(getServerDir(), 'webserver/webapps/'+war.baseName.toLowerCase()) } +project.ext.getDeployLoc = { new File(project.ext.getServerDir(), 'addons/'+war.baseName.toLowerCase()) } // task that deletes the .war (and exploded dir) from the webserver -task cleanDeploy(description: 'Deletes the war from the webserver', overwrite: true, type:Delete) -cleanDeploy.doFirst { delete getDeployLoc() } +//task cleanDeploy(description: 'Deletes the war from the webserver', overwrite: true, type:Delete) +//cleanDeploy.doFirst { delete project.ext.getDeployLoc() } // task that builds and deploys the .war to the webserver -task deploy(description: 'Deploys the war to the webserver', type: Sync, dependsOn:war) { - doFirst { - println "Deploying $war.baseName to ${getDeployLoc().canonicalPath}" - } - from zipTree(war.archivePath) - into { getDeployLoc() } - fileMode = 0644 +task deploy(description: 'Deploys the war to the webserver', dependsOn:[sourceSets.main.runtimeClasspath, generateAddOnInfo]) { + doLast { + + project.copy { + with addon + into { project.ext.getDeployLoc() } + fileMode = 0644 + } + } +} +deploy.doLast { + println "Deployed $war.baseName to ${project.ext.getDeployLoc().canonicalPath}" } -// have build tell you where the archive was put! -build.doLast { - println "War created as ${war.archivePath}" +// have war tell you where the archive was put! +war.doLast { + println "War created as ${war.archivePath}" } -task wrapper(type: Wrapper) { - gradleVersion = '1.0-milestone-9' - jarFile = file('wrapper/gradle-wrapper.jar') - archiveBase = Wrapper.PathBase.GRADLE_USER_HOME +// if needed, specify a special repo for the keystore +if (hasProperty('addon_sign_repo')) { + repositories { maven { url addon_sign_repo } } } -File pickServerDir() { - def serverDir = null; - def builder = new groovy.swing.SwingBuilder() - builder.registerBeanFactory('folderChooser', com.jidesoft.swing.FolderChooser) - builder.build { - frame(id: 'mainframe', title:'Pick Server Root Dir', defaultCloseOperation:JFrame.EXIT_ON_CLOSE, size:[440,440], locationRelativeTo: null, show: true) { - panel(border: emptyBorder(10)) { - boxLayout axis: BoxLayout.Y_AXIS - panel { - boxLayout axis: BoxLayout.X_AXIS - label text: 'In order to deploy the add-on to the server (and to run some tests), the build needs to know where the root directory of the server is located.' +// Allows add-on to request signing. Only works for official add-ons. +ext.signAddon = { + // cannot sign jars with duplicate files, so make sure that doesn't happen + addon { + duplicatesStrategy = "exclude" + } + + // add a task to perform the actual signing + def requiredSigningProperties = ['addon_sign_keystore_gav', 'addon_sign_alias', 'addon_sign_password', 'addon_sign_keypass'] + task signArchive(description: 'Signs the add-on archive', dependsOn: war) << { + if (!requiredSigningProperties.every { project.hasProperty(it) }) + { + println "All signing properties not defined, the built add-on will not be signed." + println "The required prooperties are:" + requiredSigningProperties.each { println " "+it } } - rigidArea size: new Dimension(0, 10) - panel { - boxLayout axis: BoxLayout.Y_AXIS - panel { - boxLayout axis: BoxLayout.X_AXIS - label text: 'Server root dir:' - hglue() - } - rigidArea size: new Dimension(0, 5) - panel { - boxLayout axis: BoxLayout.X_AXIS - folderChooser id: 'driverDirChooser', currentDirectory: new File('.'), navigationFieldVisible: false, controlButtonsAreShown: false, availableButtons: 0, recentListVisible: false - hglue() - } + else + { + def keyDep = dependencies.create(addon_sign_keystore_gav) + def keystore = configurations.detachedConfiguration(keyDep).resolve().iterator().next() + ant.signjar(jar: addon.archivePath, alias:addon_sign_alias, storepass:addon_sign_password, keystore:keystore.path, keypass:addon_sign_keypass) } - rigidArea size: new Dimension(0, 10) - panel { - boxLayout axis: BoxLayout.X_AXIS - hglue() - button text: 'Accept', actionPerformed: { - serverDir = builder.driverDirChooser.selectedFolder - dispose() - } - hglue() - button text: 'Cancel', actionPerformed: { - dispose() - } - hglue() + } + + // hook in the signArchive task to occur whenever a build is performed + project.afterEvaluate { + tasks['build'].dependsOn signArchive + } + + // if signing, allow the add-on to specify it's own licensing requirements (including none). The server + // will only honor this data if the add-on is signed with the valid private key. + project.convention.plugins.addoninfo.addExtension('licenseRequirements', new LicenseRequirementsExtension()) +} + +File pickServerDir() { + def serverDir = null; + def builder = new groovy.swing.SwingBuilder() + builder.registerBeanFactory('folderChooser', com.jidesoft.swing.FolderChooser) + builder.build { + frame(id: 'mainframe', title:'Pick Server Root Dir', defaultCloseOperation:JFrame.EXIT_ON_CLOSE, size:[440,440], locationRelativeTo: null, show: true) { + panel(border: emptyBorder(10)) { + boxLayout axis: BoxLayout.Y_AXIS + panel { + boxLayout axis: BoxLayout.X_AXIS + label text: 'In order to deploy the add-on to the server (and to run some tests), the build needs to know where the root directory of the server is located.' + } + rigidArea size: new Dimension(0, 10) + panel { + boxLayout axis: BoxLayout.Y_AXIS + panel { + boxLayout axis: BoxLayout.X_AXIS + label text: 'Server root dir:' + hglue() + } + rigidArea size: new Dimension(0, 5) + panel { + boxLayout axis: BoxLayout.X_AXIS + folderChooser id: 'driverDirChooser', currentDirectory: new File('.'), navigationFieldVisible: false, controlButtonsAreShown: false, availableButtons: 0, recentListVisible: false + hglue() + } + } + rigidArea size: new Dimension(0, 10) + panel { + boxLayout axis: BoxLayout.X_AXIS + hglue() + button text: 'Accept', actionPerformed: { + serverDir = builder.driverDirChooser.selectedFolder + dispose() + } + hglue() + button text: 'Cancel', actionPerformed: { + dispose() + } + hglue() + } + } } - } } - } - // block until the gui is closed - while (builder.mainframe.isVisible()) { - Thread.sleep(100) - } - if (serverDir == null) - throw new GradleException("ERROR: Build cannot continue because the root directory of the server was not specified") - return serverDir + // block until the gui is closed + while (builder.mainframe.isVisible()) { + Thread.sleep(100) + } + if (serverDir == null) + throw new GradleException("ERROR: Build cannot continue because the root directory of the server was not specified") + return serverDir } def projRef = project rootProject.idea { - project { - //if you want to set specific jdk and language level - projRef.afterEvaluate { - jdkName = "${projRef.sourceCompatibility}" - languageLevel = "${projRef.sourceCompatibility}" - } + project { + //if you want to set specific jdk and language level + projRef.afterEvaluate { + jdkName = "${projRef.sourceCompatibility}" + languageLevel = "${projRef.sourceCompatibility}" + } - ipr.withXml { - def node = it.asNode() + ipr.withXml { + def node = it.asNode() - def vcsConfig = node.component.find { it.'@name' == 'VcsDirectoryMappings' } - vcsConfig.mapping[0].'@vcs' = 'Git' + def vcsConfig = node.component.find { it.'@name' == 'VcsDirectoryMappings' } + vcsConfig.mapping[0].'@vcs' = 'Git' - def mgrNode = node.component.find { it.'@name' == 'ArtifactManager' } - if (!mgrNode) - mgrNode = node.appendNode('component', [name: 'ArtifactManager']) + def mgrNode = node.component.find { it.'@name' == 'ArtifactManager' } + if (!mgrNode) + mgrNode = node.appendNode('component', [name: 'ArtifactManager']) - def webNode = mgrNode?.artifact.find { it.'@name' == 'Add-On Web exploded' } - if (webNode) - mgrNode.remove(webNode) + def webNode = mgrNode?.artifact.find { it.'@name' == 'Add-On Web exploded' } + if (webNode) + mgrNode.remove(webNode) - mgrNode.append(new XmlParser().parseText(""" + mgrNode.append(new XmlParser().parseText(""" - ${pathFactory.relativePath('PROJECT_DIR', getDeployLoc()).relPath} + ${pathFactory.relativePath('PROJECT_DIR', new File(rootProject.getDeployLoc(), 'webapp')).relPath} - + """)) - def libNode = mgrNode.artifact.root.element.find { it.'@name' == 'WEB-INF' }.element.find { it.'@name' == 'lib' } - def userHome = new File(System.getProperty("user.home")) - modules[0].resolveDependencies().each { dep -> - if (dep instanceof org.gradle.plugins.ide.idea.model.ModuleLibrary) - { - if (dep.scope == 'COMPILE' || dep.scope == 'RUNTIME') - dep.classes.each { - libNode.appendNode('element', [id: 'file-copy', path: pathFactory.resolvePath(userHome, '$USER_HOME$', it.file).relPath]) + def libNode = mgrNode.artifact.root.element.find { it.'@name' == 'WEB-INF' }.element.find { it.'@name' == 'lib' } + def userHome = new File(System.getProperty("user.home")) + modules[0].resolveDependencies().each { dep -> + if (dep instanceof org.gradle.plugins.ide.idea.model.ModuleLibrary) + { + if (dep.scope == 'COMPILE' || dep.scope == 'RUNTIME') + dep.classes.each { + libNode.appendNode('element', [id: 'file-copy', path: pathFactory.resolvePath(userHome, '$USER_HOME$', it.file).relPath]) + } + } + } + def jsLibs = modules[0].project.configurations.js.resolve() + if (!jsLibs.isEmpty()) + { + def jsNode = mgrNode.artifact.root.element.find { it.'@name' == jsDir } + if (!jsNode) + jsNode = mgrNode.artifact.root[0].appendNode('element', [id: 'directory', name: jsDir]) + + jsLibs.each { + jsNode.appendNode('element', [id: 'file-copy', path: pathFactory.resolvePath(userHome, '$USER_HOME$', it).relPath]) + } } } - } - if (modules[0].project.configurations.findByName('js')) - { - def jsLibs = modules[0].project.configurations.js.resolve() - if (!jsLibs.isEmpty()) - { - def jsNode = mgrNode.artifact.root.element.find { it.'@name' == jsDir } - if (!jsNode) - jsNode = mgrNode.artifact.root[0].appendNode('element', [id: 'directory', name: jsDir]) - - jsLibs.each { - jsNode.appendNode('element', [id: 'file-copy', path: pathFactory.resolvePath(userHome, '$USER_HOME$', it).relPath]) - } - } - } } - } } idea { - module { - scopes.PROVIDED.plus += [configurations.providedCompile] - scopes.PROVIDED.plus += [configurations.providedRuntime] - scopes.COMPILE.minus += [configurations.providedCompile] - scopes.RUNTIME.minus += [configurations.providedRuntime] - - inheritOutputDirs = false - outputDir = file("$buildDir/classes/main") - testOutputDir = file("$buildDir/classes/test") - - iml.withXml { - def node = it.asNode() - - def mgrNode = node.component.find { it.'@name' == 'FacetManager' } - if (!mgrNode) - mgrNode = node.appendNode('component', [name: 'FacetManager']) - def webNode = mgrNode?.facet.find { it.'@name' == 'Add-On Web' } - if (webNode) - mgrNode.remove(webNode) - - mgrNode.append(new XmlParser().parseText(''' + module { + scopes.PROVIDED.plus += [configurations.providedCompile] + scopes.PROVIDED.plus += [configurations.providedRuntime] + scopes.COMPILE.minus += [configurations.providedCompile] + scopes.RUNTIME.minus += [configurations.providedRuntime] + + inheritOutputDirs = false + outputDir = file("$buildDir/classes/main") + testOutputDir = file("$buildDir/classes/test") + + iml.withXml { + def node = it.asNode() + + def mgrNode = node.component.find { it.'@name' == 'FacetManager' } + if (!mgrNode) + mgrNode = node.appendNode('component', [name: 'FacetManager']) + def webNode = mgrNode?.facet.find { it.'@name' == 'Add-On Web' } + if (webNode) + mgrNode.remove(webNode) + + mgrNode.append(new XmlParser().parseText(''' @@ -301,37 +349,158 @@ idea { ''')) + } } - } } buildscript { - repositories { mavenCentral() } + + if( project.hasProperty( 'defineBuildScriptRepositories' ) ) + project.ext.defineBuildScriptRepositories( repositories ) + else + repositories { mavenCentral() } + dependencies { classpath group: 'com.jidesoft', name: 'jide-oss', version: '2.9.0' } } -// Support for addon-info.xml +// Support for info.xml class AddOnInfo { - def String name - def String description - def String version - def String vendor - def String systemMenuProvider - - void info(Closure c) { - c.setDelegate(this) - c.setResolveStrategy Closure.DELEGATE_ONLY - c() - } - - String propNameToXmlElementName(String propName) - { - // this converts a camelCase string to dash-separated all lower case string (i.e. camel-case) - propName.replaceAll('[A-Z]', { '-'+it.toLowerCase() }) - } - - Map getProperties() { - [ name:name, description:description, version:version, vendor:vendor, systemMenuProvider:systemMenuProvider ] - } + def String name + def String description + def String version + def String vendor + Map extensions = [:] + def supportedApplications = [] + + void info(Closure c) { + c.setDelegate(this) + c.setResolveStrategy Closure.DELEGATE_ONLY + c() + } + + def addExtension(name, obj) { + extensions.put(name, obj) + } + + // for backwards compatibility + def propertyMissing(String name, value) { + if (name == 'systemMenuProvider') + extensions.get('providers')?.systemMenu = value + else + throw new MissingPropertyException(name) + } + + def methodMissing(String name, args) { + if (args[0] instanceof Closure) + { + def ext = extensions.get(name) + if (ext) { + args[0].setDelegate(ext) + args[0].setResolveStrategy Closure.DELEGATE_ONLY + return args[0].call() + } + } + + throw new MissingMethodException(name, delegate, args) + } + + Map getProperties() { + Map properties = + [ name:name, description:description, version:version, vendor:vendor, + extensions:extensions.collectEntries { key, value -> [(key) : value.getProperties()] }, + ] + + addSupportedApplicationsToMap( properties ) + + return properties; + } + + /** + * This handles specifying supported applications -- as we now have more than just WebCTRL + * add-ons. This was introduced in a WebCTRL 6.5 patch but needs to be backward compatible. + * To handle this, if you don't specify anything in supportedApplications, this will generate + * the same info.xml as it always did (which means WebCTRL will assume that webserver is the + * only supported application). If however, you specify anything in supportedApplications, + * then you must specify ALL applications you support. + */ + private void addSupportedApplicationsToMap( Map properties ) + { + if( supportedApplications.isEmpty() ) + return; + + supportedApplications.each { + properties.put( it, 'supported' ) + } + } + + private Map getPropMap() { + def map = [:] + if (name) + map['name'] = name + if (description) + map['description'] = description + if (version) + map['version'] = version + if (vendor) + map['vendor'] = vendor + return map + } + + void addXml(root) { + propMap.each { key, value -> + root."$key" { root.mkp.yield(value) } + } + extensions.values().each { it.addXml(root) } + + addSupportedApplicationsXml( root ) + } + + //see addSupportedApplicationsToMap. Same concept. + def addSupportedApplicationsXml( root ) + { + if( supportedApplications.isEmpty() ) + return; + + root.supports { + def supportedElement = delegate; + supportedApplications.each { app -> + supportedElement.application( "${app}" ) + } + } + } } +// support for providers in info.xml +class ProviderExtension { + Map providers = [:] + + String propNameToXmlElementName(String propName) + { + // this converts a camelCase string to dash-separated all lower case string (i.e. camel-case) + propName.replaceAll('[A-Z]', { '-'+it.toLowerCase() }) + '-provider' + } + + def propertyMissing(name, value) { providers.put(name, value) } + Map getProperties() { return providers; } + void addXml(root) { + providers.each { String key, value -> + root."${propNameToXmlElementName(key)}" { root.mkp.yield(value) } + } + } +} + +// support for license requirements in info.xml (only available if signing is requested) +class LicenseRequirementsExtension { + Map entries = [:] + + def methodMissing(String name, args) { entries.put(name, args) } + Map getProperties() { [entries: entries] } + void addXml(root) { + root.'license-requirements' { + def xmlNode = delegate + entries.each { name, args -> + xmlNode.invokeMethod(name, args) + } + } + } +}