Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 98aea9915c
Fetching contributors…

Cannot retrieve contributors at this time

627 lines (540 sloc) 26.162 kb
/*
* Copyright 2004-2005 the original author or authors.
*
* 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.
*/
package org.codehaus.groovy.grails.resolve
import grails.util.PluginBuildSettings
import grails.util.BuildSettings
import grails.util.Metadata
import grails.util.GrailsNameUtils
import org.codehaus.groovy.grails.plugins.GrailsPluginUtils
import grails.util.GrailsUtil
import org.springframework.core.io.Resource
import org.codehaus.groovy.grails.cli.CommandLineHelper
import groovy.util.slurpersupport.GPathResult
import org.codehaus.groovy.grails.cli.ScriptExitException
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import org.apache.ivy.core.report.ResolveReport
import org.apache.ivy.core.module.id.ModuleRevisionId
import org.apache.ivy.core.module.descriptor.DependencyDescriptor
import org.apache.ivy.core.report.ArtifactDownloadReport
/**
* This class is used to manage the installation and uninstallation of plugins
* from a Grails project
*
* @author Graeme Rocher
* @since 1.3
*/
class PluginInstallEngine {
static final CORE_PLUGINS = ['core', 'i18n','converters','mimeTypes', 'controllers','webflow', 'dataSource', 'domainClass', 'filters','logging', 'groovyPages']
Closure errorHandler = { String msg -> throw new ScriptExitException(msg) }
Closure eventHandler = { String name, String msg -> println msg }
Closure pluginScriptRunner
Closure postInstallEvent
Closure postUninstallEvent
/**
* plugins that were installed in the last execution of installPlugin
*/
List installedPlugins = []
def pluginDirVariableStore = [:]
boolean isInteractive = true
CommandLineHelper commandLineHelper = new CommandLineHelper(System.out)
protected Metadata metadata
protected PluginBuildSettings pluginSettings
protected BuildSettings settings
protected applicationPluginsLocation
protected globalPluginsLocation
protected ant
protected PluginResolveEngine resolveEngine
PluginInstallEngine(grails.util.BuildSettings settings) {
this(settings, new PluginBuildSettings(settings), Metadata.current, new AntBuilder())
}
PluginInstallEngine(grails.util.BuildSettings settings, grails.util.PluginBuildSettings pbs) {
this(settings, pbs, Metadata.current, new AntBuilder())
}
PluginInstallEngine(grails.util.BuildSettings settings, grails.util.PluginBuildSettings pbs, Metadata md) {
this(settings, pbs, md, new AntBuilder())
}
PluginInstallEngine(grails.util.BuildSettings settings, grails.util.PluginBuildSettings pbs, Metadata md, AntBuilder ant) {
if(settings == null) throw new IllegalArgumentException("Argument [settings] cannot be null")
if(pbs == null) throw new IllegalArgumentException("Argument [pbs] cannot be null")
if(md == null) throw new IllegalArgumentException("Argument [md] cannot be null")
if(ant== null) throw new IllegalArgumentException("Argument [ant] cannot be null")
globalPluginsLocation = settings.globalPluginsDir
applicationPluginsLocation = settings.getProjectPluginsDir()
pluginSettings = pbs
this.settings = settings
this.ant = ant
this.metadata = md
this.resolveEngine = new PluginResolveEngine(settings.dependencyManager, settings)
}
/**
* This method will resolve the current dependencies and install any missing plugins or upgrades
* and remove any plugins that aren't present in the metadata but are installed
*/
void resolvePluginDependencies() {
IvyDependencyManager dependencyManager = settings.dependencyManager
List pluginDeps = dependencyManager.pluginDependencyDescriptors.collect { DependencyDescriptor dd ->
dd.getDependencyRevisionId()
}
List pluginsToInstall = findMissingOrUpgradePlugins(pluginDeps)
installPlugins(pluginsToInstall)
checkPluginsToUninstall(pluginDeps)
}
/**
* Installs a list of plugins
*
* @param params A list of plugins defined each by a ModuleRevisionId
*/
void installPlugins(List<ModuleRevisionId> plugins) {
if (plugins) {
eventHandler "StatusUpdate", "Resolving new plugins. Please wait..."
ResolveReport report = resolveEngine.resolvePlugins(plugins)
if (report.hasError()) {
errorHandler "Failed to resolve plugins."
}
else {
for (ArtifactDownloadReport ar in report.allArtifactsReports) {
def arName = ar.artifact.moduleRevisionId.name
if (plugins.any { it.name == arName }) {
installPlugin ar.localFile
}
}
}
}
}
/**
* Installs a plugin for the given name and optional version
*
* @param name The plugin name
* @param version The plugin version (optional)
* @param globalInstall Whether to install globally or not (optional)
*/
void installPlugin(String name, String version = null, boolean globalInstall = false) {
installedPlugins.clear()
def pluginZip = resolveEngine.resolvePluginZip(name, version)
if(!pluginZip) {
errorHandler "Plugin not found for name [$name] and version [${version ?: 'not specified'}]"
}
try {
(name, version) = readMetadataFromZip(pluginZip.absolutePath)
installPluginZipInternal(name, version, pluginZip, globalInstall)
}
catch (e) {
errorHandler "Error installing plugin: ${e.message}"
}
}
/**
* Installs a plugin from the given ZIP file
*
* @param zipFile The plugin zip file
* @param globalInstall Whether it is a global install or not (optional)
*/
void installPlugin(File zipFile, boolean globalInstall = false) {
if(zipFile.exists()) {
def (name, version) = readMetadataFromZip(zipFile.absolutePath)
installPluginZipInternal name, version, zipFile, globalInstall
}
else {
errorHandler "Plugin zip not found at location: ${zipFile.absolutePath}"
}
}
/**
* Installs a plugin from the given URL
*
* @param zipURL The zip URL
* @param globalInstall Whether it is a global install or not (optional)
*/
void installPlugin(URL zipURL, boolean globalInstall = false) {
def s = zipURL.toString()
def filename = s[s.lastIndexOf("/")..-1]
def file = File.createTempFile(filename[0..-4], ".zip")
file.deleteOnExit()
eventHandler "StatusUpdate", "Downloading zip ${zipURL}. Please wait..."
try {
ant.get(src: zipURL, dest: file, verbose:"on")
}
catch (e) {
errorHandler "Error downloading plugin ${zipURL}: ${e.message}"
}
installPlugin(file, globalInstall)
}
protected void installPluginZipInternal(String name, String version, File pluginZip, boolean globalInstall) {
def fullPluginName = "$name-$version"
def pluginInstallPath = "${globalInstall ? globalPluginsLocation : applicationPluginsLocation}/${fullPluginName}"
assertNoExistingInlinePlugin(name)
def abort = checkExistingPluginInstall(name, version)
if(abort) {
registerPluginWithMetadata(name, version)
return
}
eventHandler "StatusUpdate", "Installing zip ${pluginZip}..."
installedPlugins << pluginInstallPath
if (isNotInlinePluginLocation(new File(pluginInstallPath))) {
ant.delete(dir: pluginInstallPath, failonerror: false)
ant.mkdir(dir: pluginInstallPath)
ant.unzip(dest: pluginInstallPath, src: pluginZip)
eventHandler "StatusUpdate", "Installed plugin ${fullPluginName} to location ${pluginInstallPath}."
}
else {
errorHandler "Cannot install plugin. Plugin install would override inline plugin configuration which is not allowed."
}
def pluginXmlFile = new File("${pluginInstallPath}/plugin.xml")
if (!pluginXmlFile.exists()) {
errorHandler("Plugin $fullPluginName is not a valid Grails plugin. No plugin.xml descriptor found!")
}
def pluginXml = new XmlSlurper().parse(pluginXmlFile)
def pluginName = pluginXml.@name.toString()
def pluginVersion = pluginXml.@version.toString()
def pluginGrailsVersion = pluginXml.@grailsVersion.toString()
assertGrailsVersionValid(fullPluginName, pluginGrailsVersion)
// Add the plugin's directory to the binding so that any event
// handlers in the plugin have access to it. Normally, this
// variable is added in GrailsScriptRunner, but this plugin
// hasn't been installed by that point.
pluginDirVariableStore["${GrailsNameUtils.getPropertyNameForLowerCaseHyphenSeparatedName(pluginName)}PluginDir"] = new File(pluginInstallPath).absoluteFile
def dependencies = processPluginDependencies(pluginName,pluginXml)
// if there are any unprocessed dependencies, bail out
if (dependencies) {
ant.delete(dir: "${pluginInstallPath}", quiet: true, failOnError: false)
errorHandler("Failed to install plugin [${fullPluginName}]. Missing dependencies: ${dependencies.inspect()}")
}
else {
resolvePluginJarDependencies(fullPluginName, pluginInstallPath)
// proceed _Install.groovy plugin script if exists
def installScript = new File("${pluginInstallPath}/scripts/_Install.groovy")
runPluginScript(installScript, fullPluginName, "post-install script")
registerPluginWithMetadata(pluginName, pluginVersion)
displayNewScripts(fullPluginName, pluginInstallPath)
postInstall(pluginInstallPath)
eventHandler("PluginInstalled", fullPluginName)
}
}
/**
* Reads plugin metadata from a plugin zip file and returns a list containing the plugin name, version and
* XML metadata. Designed for use with Groovy's multiple assignment operator
*
* @param zipLocation The zip location
* @return A list
*/
List readMetadataFromZip(String zipLocation) {
try {
def zipFile = new ZipFile(zipLocation)
ZipEntry entry = zipFile.entries().find {ZipEntry entry -> entry.name == 'plugin.xml'}
if (entry) {
def pluginXml = new XmlSlurper().parse(zipFile.getInputStream(entry))
def name = pluginXml.'@name'.text()
def release = pluginXml.'@version'.text()
return [name, release, pluginXml]
}
else {
errorHandler("Plugin $zipLocation is not a valid Grails plugin. No plugin.xml descriptor found!")
}
}
catch (e) {
errorHandler("Error reading plugin zip [$zipLocation]. The plugin zip file may be corrupt.")
}
}
/**
* Checks an existing plugin path to install and returns true if the installation should be aborted or false if it should continue
*
* If an error occurs the errorHandler is invoked.
*
* @param name The plugin name
* @param version The plugin version
* @return True if the installation should be aborted
*/
protected boolean checkExistingPluginInstall(String name, version) {
Resource currentInstall = pluginSettings.getPluginDirForName(name)
if (currentInstall?.exists()) {
PluginBuildSettings pluginSettings = pluginSettings
def pluginDir = currentInstall.file.canonicalFile
def pluginInfo = pluginSettings.getPluginInfo(pluginDir.absolutePath)
// if the versions are the same no need to continue
if(version == pluginInfo?.version) return true
if (pluginSettings.isInlinePluginLocation(currentInstall)) {
errorHandler("The plugin you are trying to install [$name-${version}] is already configured as an inplace plugin in grails-app/conf/BuildConfig.groovy. You cannot overwrite inplace plugins.");
return true
}
else if (!isInteractive || confirmInput("You currently already have a version of the plugin installed [$pluginDir.name]. Do you want to upgrade this version?")) {
ant.delete(dir: currentInstall.file)
}
else {
errorHandler("Plugin $name-$version install aborted");
return true
}
}
return false
}
protected void assertNoExistingInlinePlugin(String name) {
def pluginReference = settings.config.grails.plugin.location[name]
if (pluginReference) {
errorHandler("""\
Plugin [$name] is aliased as [grails.plugin.location.$name] to the location [$pluginReference] in grails-app/conf/BuildConfig.groovy.
You cannot upgrade a plugin that is configured via BuildConfig.groovy, remove the configuration to continue.""");
}
}
protected void displayNewScripts(pluginName, installPath) {
def providedScripts = new File("${installPath}/scripts").listFiles().findAll { !it.name.startsWith('_') && it.name.endsWith(".groovy")}
eventHandler("StatusFinal", "Plugin ${pluginName} installed")
if (providedScripts) {
println "Plugin provides the following new scripts:"
println "------------------------------------------"
providedScripts.each { File file ->
def scriptName = GrailsNameUtils.getScriptName(file.name)
println "grails ${scriptName}"
}
}
}
protected void resolvePluginJarDependencies(pluginName, pluginInstallPath) {
def pluginDependencyDescriptor = new File("$pluginInstallPath/dependencies.groovy")
if (pluginDependencyDescriptor.exists()) {
eventHandler "StatusUpdate", "Resolving plugin JAR dependencies"
def callable = settings.pluginDependencyHandler()
callable.call(new File("$pluginInstallPath"))
IvyDependencyManager dependencyManager = settings.dependencyManager
dependencyManager.resetGrailsPluginsResolver()
def resolveReport = dependencyManager.resolveDependencies(IvyDependencyManager.RUNTIME_CONFIGURATION)
if (resolveReport.hasError()) {
errorHandler("Failed to install plugin [${pluginName}]. Plugin has missing JAR dependencies.")
}
else {
addJarsToRootLoader resolveReport.allArtifactsReports.localFile
}
}
def pluginJars = new File("${pluginInstallPath}/lib").listFiles().findAll { it.name.endsWith(".jar")}
addJarsToRootLoader(pluginJars)
}
protected def addJarsToRootLoader(Collection pluginJars) {
def loader = getClass().classLoader.rootLoader
for (File jar in pluginJars) {
loader.addURL(jar.toURI().toURL())
}
}
protected Map processPluginDependencies(String pluginName, GPathResult pluginXml) {
Map dependencies = [:]
for (dep in pluginXml.dependencies.plugin) {
def depName = dep.@name.toString()
String depVersion = dep.@version.toString()
if (isCorePlugin(depName)) {
def grailsVersion = GrailsUtil.getGrailsVersion()
if (!GrailsPluginUtils.isValidVersion(grailsVersion, depVersion))
errorHandler("Plugin requires version [$depVersion] of Grails core, but installed version is [${grailsVersion}]. Please upgrade your Grails installation and try again.")
}
else {
dependencies[depName] = depVersion
def depDirName = GrailsNameUtils.getScriptName(depName)
def manager = settings.dependencyManager
if(manager.isExcludedFromPlugin(pluginName, depDirName)) {
dependencies.remove(depName)
}
else {
def depPluginDir = pluginSettings.getPluginDirForName(depDirName)?.file
if (!depPluginDir?.exists()) {
eventHandler("StatusUpdate", "Plugin dependency [$depName] not found. Attempting to resolve...")
// recursively install dependent plugins
def upperVersion = GrailsPluginUtils.getUpperVersion(depVersion)
def installVersion = upperVersion
if (installVersion == '*') {
installVersion = settings.defaultPluginSet.contains(depDirName) ? GrailsUtil.getGrailsVersion() : null
}
// recurse
installPlugin(depDirName, installVersion)
dependencies.remove(depName)
}
else {
def dependency = readPluginXmlMetadata(depDirName)
def dependencyVersion = dependency.@version.toString()
if (!GrailsPluginUtils.isValidVersion(dependencyVersion, depVersion)) {
errorHandler("Plugin requires version [$depVersion] of plugin [$depName], but installed version is [${dependencyVersion}]. Please upgrade this plugin and try again.")
}
else {
dependencies.remove(depName)
}
}
}
}
}
return dependencies
}
protected readPluginXmlMetadata(String pluginDirName) {
def pluginDir = pluginSettings.getPluginDirForName(pluginDirName)?.file
new XmlSlurper().parse(new File("${pluginDir}/plugin.xml"))
}
private assertGrailsVersionValid(String pluginName, String grailsVersion) {
if (grailsVersion) {
if (!GrailsPluginUtils.isValidVersion(GrailsUtil.grailsVersion, grailsVersion)) {
errorHandler("Plugin $pluginName requires version [${grailsVersion}] of Grails which your current Grails installation does not meet. Please try install a different version of the plugin or Grails.")
}
}
}
/**
* Uninstalls a plugin for the given name and optional version
*
* @param name The plugin name
* @param version The version
*/
void uninstallPlugin(String name, String version = null) {
try {
String pluginKey = "plugins.$name"
metadata.remove(pluginKey)
metadata.persist()
def pluginDir
if(name && version) {
pluginDir = new File("${applicationPluginsLocation}/$name-$version")
}
else {
pluginDir = pluginSettings.getPluginDirForName(name)?.file
}
if(pluginDir?.exists()) {
def uninstallScript = new File("${pluginDir}/scripts/_Uninstall.groovy")
runPluginScript(uninstallScript, pluginDir.name, "uninstall script")
if (isNotInlinePluginLocation(pluginDir)) {
ant.delete(dir:pluginDir, failonerror:true)
}
postUninstall()
eventHandler "PluginUninstalled", "Uninstalled plugin [${name}]."
}
else {
errorHandler("No plugin [$name${version ? '-' + version : ''}] installed, cannot uninstall")
}
}
catch (e) {
e.printStackTrace()
errorHandler("An error occured installing the plugin [$name${version ? '-' + version : ''}]: ${e.message}")
}
}
/**
* Registers a plugin name and version as installed according to the plugin metadata
* @param pluginName the name of the plugin
* @param pluginVersion the version of the plugin
*/
void registerPluginWithMetadata(String pluginName, pluginVersion) {
IvyDependencyManager dependencyManager = settings.getDependencyManager()
// only register in metadata if not specified in BuildConfig.groovy
if(dependencyManager.metadataRegisteredPluginNames?.contains(pluginName)) {
metadata['plugins.' + pluginName] = pluginVersion
metadata.persist()
}
}
private void runPluginScript( File scriptFile, fullPluginName, msg ) {
if(pluginScriptRunner != null) {
if(pluginScriptRunner.maximumNumberOfParameters < 3) {
throw new IllegalStateException("The [pluginScriptRunner] closure property must accept at least 3 arguments")
}
else {
pluginScriptRunner.call(scriptFile, fullPluginName, msg)
}
}
}
/**
* Check to see if the plugin directory is in plugins home.
*/
boolean isNotInlinePluginLocation(File pluginDir) {
// insure all the directory is in the pluginsHome
checkPluginPathWithPluginDir(applicationPluginsLocation, pluginDir) ||
checkPluginPathWithPluginDir(globalPluginsLocation, pluginDir)
}
protected postUninstall() {
// Update the cached dependencies in grailsSettings, and add new jars to the root loader
resolveDependenciesAgain()
pluginSettings.clearCache()
postUninstallEvent?.call()
}
protected postInstall(String pluginInstallPath) {
// Update the cached dependencies in grailsSettings, and add new jars to the root loader
pluginSettings.clearCache()
postInstallEvent?.call(pluginInstallPath)
}
private checkPluginPathWithPluginDir (File pluginsHome, File pluginDir) {
def absPluginsHome = pluginsHome.absolutePath
pluginDir.absolutePath.startsWith(absPluginsHome)
}
private void resolveDependenciesAgain() {
for (type in ['compile', 'build', 'test', 'runtime']) {
def existing = settings."${type}Dependencies"
def all = settings.dependencyManager.resolveDependencies(IvyDependencyManager."${type.toUpperCase()}_CONFIGURATION").allArtifactsReports.localFile
def toAdd = all - existing
if (toAdd) {
existing.addAll(toAdd)
if (type in ['build', 'test']) {
toAdd.each {
settings.rootLoader.addURL(it.toURL())
}
}
}
}
}
private boolean isCorePlugin(name) {
CORE_PLUGINS.contains(name)
}
private boolean confirmInput(String msg) {
commandLineHelper.userInput(msg, ['y','n'] as String[]) == 'y'
}
protected void checkPluginsToUninstall(List<DependencyDescriptor> pluginDeps) {
// Find out which plugins are in the search path but not in the
// metadata. We only check on the plugins in the project's "plugins"
// directory and the global "plugins" dir. Plugins loaded via an
// explicit path should be left alone.
def pluginDirs = pluginSettings.implicitPluginDirectories
def pluginsToUninstall = pluginDirs.findAll { Resource r ->
!pluginDeps.find { ModuleRevisionId plugin ->
r.filename ==~ "$plugin.name-.+"
}
}
for (Resource pluginDir in pluginsToUninstall) {
if (pluginSettings.isGlobalPluginLocation(pluginDir)) {
registerMetadataForPluginLocation(pluginDir)
}
else {
if (!isInteractive || confirmInput("Plugin [${pluginDir.filename}] is installed, but was not found in the application's metadata, do you want to uninstall?")) {
uninstallPlugin(pluginDir.filename)
}
else {
registerMetadataForPluginLocation(pluginDir)
}
}
}
}
protected List<ModuleRevisionId> findMissingOrUpgradePlugins(List pluginDeps) {
def pluginsToInstall = []
for (p in pluginDeps) {
def name = p.name
def version = p.revision
def fullName = "$name-$version"
def pluginLoc = pluginSettings.getPluginDirForName(name)
if (!pluginLoc?.exists()) {
eventHandler "StatusUpdate", "Plugin [${fullName}] not installed."
pluginsToInstall << p
}
else if (pluginLoc) {
def dirName = pluginLoc.file.canonicalFile.name
PluginBuildSettings settings = pluginSettings
if (!dirName.endsWith(version) && !settings.isInlinePluginLocation(pluginLoc)) {
// only print message if the version doesn't start with "latest." since we have
// to do a check for a new version when there version is specified as "latest.integration" etc.
if(!version.startsWith("latest."))
eventHandler "StatusUpdate", "Upgrading plugin [$dirName] to [${fullName}]."
pluginsToInstall << p
}
}
}
return pluginsToInstall
}
private registerMetadataForPluginLocation(Resource pluginDir) {
def plugin = pluginSettings.getMetadataForPlugin(pluginDir.filename)
registerPluginWithMetadata(plugin.@name.text(), plugin.@version.text())
}
}
Jump to Line
Something went wrong with that request. Please try again.