/*******************************************************************************
* Copyright (c) 2012 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.core.internal.classpath;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.grails.ide.eclipse.commands.GrailsCommand;
import org.grails.ide.eclipse.commands.GrailsCommandFactory;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
import org.grails.ide.eclipse.core.internal.GrailsNature;
import org.grails.ide.eclipse.core.internal.plugins.GrailsCore;
import org.grails.ide.eclipse.core.internal.plugins.PerProjectPluginCache;
import org.springsource.ide.eclipse.commons.core.SpringCoreUtils;
import org.springsource.ide.eclipse.commons.frameworks.core.internal.plugins.PluginVersion;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* This class is responsible for generating a complete list of Grails plugins
* for a given project, including in-place plugins, as well as marking published
* plugins as installed if they are installed in the given project.
* <p>
* A cached list of dependencies for the project is also kept, and it is
* re-generated on every parsing operation. The cached list contains dependency
* data converted into a plugin model representation, and includes in-place
* plugin models.
* </p>
* @author Nieraj Singh
* @author Andrew Eisenberg
* @author Kris De Volder
*/
public class GrailsPluginsListManager {
public static final String NO_VERSION_ID = "";
private IProject project;
public static final String PLUGIN_NODE = "plugin";
public static final String RELEASE_NODE = "release";
public static final String TITLE_NODE = "title";
public static final String AUTHOR_NODE = "author";
public static final String DESCRIPTION_NODE = "description";
public static final String DOCUMENTATION_NODE = "documentation";
public static final String NAME_ATT = "name";
public static final String LATEST_RELEASE_ATT = "latest-release";
public static final String VERSION_ATT = "version";
private static final String[] PREINSTALLED_PLUGINS = new String[] {
"hibernate", "tomcat", "webflow" };
public GrailsPluginsListManager(IProject project) {
this.project = project;
}
public PerProjectPluginCache getDependencyCache() {
PerProjectPluginCache cache = GrailsCore.get().connect(project,
PerProjectPluginCache.class);
return cache;
}
public Collection<GrailsPlugin> getDependenciesAsPluginModels() {
return generateDependenciesAsPluginModels(null);
}
/**
* These are plugins that are preinstalled when a Grails project is created.
* These are dependent on the Grails version. Examples are hibernate and
* tomcat. Regardless of whether newer versions of these plugins exist,
* these plugins are always considered to be installed to the "latest"
* version. The are not marked as having update available, although a user
* can manually update them if necessary
*
* @return list of preinstalled plugins in the project. Never null, although
* may be empty
*/
public static Collection<String> getPreInstalledPlugins() {
return new HashSet<String>(Arrays.asList(PREINSTALLED_PLUGINS));
}
/**
* Get the manager instance for the given project, if the project is a
* Grails project. Otherwise return null;
*
* @param project
* must be Grails project
* @return manager if project is a Grails project, or null
*/
public static GrailsPluginsListManager getGrailsPluginsListManager(
IProject project) {
if (GrailsNature.isGrailsProject(project)) {
return new GrailsPluginsListManager(project);
}
return null;
}
/**
* Given a map of all plugins, merge plugin dependencies for the given
* project into the map, making sure that, among other things, installed
* versions and in-place plugins are added into the plugin map, as the
* plugin map should contain all published, in-place and installed plugin
* data.
*
* @param pluginMap
* if null, it will generate new plugin models for dependencies
* instead of using existing models in the map
* @return list of dependency plugins converted to plugin models. Never
* null, but may be empty
*/
protected List<GrailsPlugin> generateDependenciesAsPluginModels(
Map<String, GrailsPlugin> pluginMap) {
List<GrailsPlugin> cachedDependencies = new ArrayList<GrailsPlugin>();
PerProjectPluginCache cache = getDependencyCache();
if (cache != null) {
for (GrailsPluginVersion dependency : cache.getCachedDependencies()) {
// First obtain an existing plugin model
GrailsPlugin dependencyModel = pluginMap != null ? pluginMap
.get(dependency.getName()) : null;
// If the plugin model doesn't exist it is most likely an
// in-place plugin
if (dependencyModel != null) {
// The installed version MUST exist, unless it is an
// in-place plugin
PluginVersion installedVersion = dependencyModel
.getVersion(dependency.getVersion());
if (installedVersion != null) {
installedVersion.setInstalled(true);
}
} else {
dependencyModel = createPlugin(dependency);
dependencyModel.getLatestReleasedVersion().setInstalled(true);
// Handle the in-place plugin addition
String descriptor = cache
.getDependencyPluginDescriptor(dependency);
if (GrailsPluginUtil.isInPlacePluginDescriptor(descriptor)) {
dependencyModel.setIsInPlace(true);
}
if (pluginMap != null) {
pluginMap.put(dependencyModel.getName(),
dependencyModel);
}
}
cachedDependencies.add(dependencyModel);
}
}
return cachedDependencies;
}
/**
* @param pluginMap
* @param cache
* @param dependency
* @return
*/
private GrailsPlugin createPlugin(
GrailsPluginVersion dependency) {
GrailsPlugin dependencyModel = new GrailsPlugin(dependency.getName());
// create a version element to represent the single
// version of an in-place plugin
PluginVersion version = new PluginVersion(dependency);
// Add the dependency as a version, usually inplace only
// have one version: the inplace plugin itself
addVersion(version, dependencyModel);
// The single version is also the latest "released" version
dependencyModel.setLatestReleasedVersion(version);
return dependencyModel;
}
/**
* Parse the list of all available Grails plugins, including all published
* plugins as well as in-place and installed plugins for the given project.
* <p>
* If 'aggressive' is true, it will force the execution of 'list-plugins'.
* Otherwise it will try to use existing plugin list files first.
*
* @return parsed list of plugins, or null if parsed failed
*/
public Collection<GrailsPlugin> generateList(boolean aggressive) {
File[] pluginsFiles = getPluginsListFiles();
if (aggressive && pluginsFiles!=null) {
for (File file : pluginsFiles) {
file.delete();
}
pluginsFiles = null;
}
// The files may not be there therefore re-generate them by
// running "list-plugins" command
if (pluginsFiles == null || pluginsFiles.length == 0) {
//If aggressive, then pluginFiles will be null so this code will always run.
try {
GrailsCommand cmd = GrailsCommandFactory.listPlugins(project);
cmd.enableRefreshDependencyFile(); // must make sure we have dependency data, it tells us where the pluginsFolder is!
cmd.synchExec();
GrailsCore.get().connect(project, PerProjectDependencyDataCache.class).refreshData();
pluginsFiles = getPluginsListFiles();
} catch (CoreException e) {
GrailsCoreActivator.log(e);
}
}
if (pluginsFiles != null && pluginsFiles.length > 0) {
Map<String, GrailsPlugin> publishedPluginsMap = new HashMap<String, GrailsPlugin>();
for (File pluginsFile : pluginsFiles) {
if (pluginsFile.exists() && pluginsFile.canRead()) {
parseLocation(pluginsFile, publishedPluginsMap);
}
}
// now include in place plugins from the workspace
addInPlacePluginsFromWorkspace(publishedPluginsMap);
// Now merge any dependency information from the project, including
// in-place plugins
generateDependenciesAsPluginModels(publishedPluginsMap);
return publishedPluginsMap.values();
} else {
GrailsCoreActivator.log("Unable to find or generate plugins list in: "+GrailsPluginUtil.getGrailsWorkDir(project), null);
}
return null;
}
/**
* @param publishedPluginsMap
*/
private void addInPlacePluginsFromWorkspace(
Map<String, GrailsPlugin> publishedPluginsMap) {
IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
for (IProject project : allProjects) {
if (GrailsNature.isGrailsPluginProject(project) && !project.equals(this.project)) {
IPath pluginPath = GrailsNature.createPathToPluginXml(project);
GroovyPluginParser parser = new GroovyPluginParser(pluginPath);
GrailsPluginVersion data = parser.parse();
// only add if data doesn't already exists (ie- do not overwrite
// existing published plugin
if (data != null && !publishedPluginsMap.containsKey(data.getName())) {
GrailsPlugin plugin = createPlugin(data);
plugin.setIsInPlace(true);
publishedPluginsMap.put(plugin.getName(), plugin);
}
}
}
}
/**
* Get all the files the xml files that contain plugin lists. This operation may fail and
* return null a number of reasons:
* - the plugins directory could not be determined because the project's dependency data has not yet been generated
* - the plugins files have not yet been generated by grails.
*
* @param pluginsFolder
* @return
*/
private File[] getPluginsListFiles() {
File[] pluginsFiles;
pluginsFiles = null;
String pluginsDirectory = GrailsPluginUtil.getGrailsWorkDir(project);
if (pluginsDirectory!=null) {
File pluginsFolder = new File(pluginsDirectory);
if (pluginsFolder.exists() && pluginsFolder.isDirectory()
&& pluginsFolder.canRead()) {
pluginsFiles = pluginsFolder.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.startsWith("plugins-list-")
&& name.endsWith(".xml");
}
});
}
}
return pluginsFiles;
}
/**
* Given a plugin descriptor location, parse the contents and create plugin
* models for each plugin that is parsed.
*
* @param file
* @param parsedPublishedPluginData
*/
protected void parseLocation(File file,
Map<String, GrailsPlugin> parsedPublishedPluginData) {
if (file == null || !file.exists() || parsedPublishedPluginData == null) {
return;
}
try {
DocumentBuilder docBuilder = SpringCoreUtils.getDocumentBuilder();
Document doc = docBuilder.parse(file);
NodeList binaryNodes = doc.getElementsByTagName(PLUGIN_NODE);
for (int i = 0; i < binaryNodes.getLength(); i++) {
Node pluginNode = binaryNodes.item(i);
// The parent node is the latest version, therefore create a
// model for it
// first. The information may be incomplete, so keep the
// reference until
// the children of this node are parsed in which case the latest
// version
// will be added in the correct order in the list of verison as
// well
// as any additional information that is missing will also be
// added
PluginVersion latestVersion = createLatestReleasedVersionFromParentNode(pluginNode);
// If no version can be generated for this node skip it as
// something
// may have gone wrong.
if (latestVersion == null) {
continue;
}
// Create the plugin model that contains all the versions,
// including the latest
// version
String pluginName = latestVersion.getName();
GrailsPlugin parentPlugin = parsedPublishedPluginData
.get(pluginName);
if (parentPlugin == null) {
parentPlugin = new GrailsPlugin(pluginName);
}
NodeList childNodes = pluginNode.getChildNodes();
for (int j = 0; j < childNodes.getLength(); j++) {
Node releaseNode = childNodes.item(j);
if (RELEASE_NODE.equals(releaseNode.getNodeName())) {
addVersion(releaseNode, parentPlugin, latestVersion);
}
}
parentPlugin.setLatestReleasedVersion(latestVersion);
parsedPublishedPluginData.put(parentPlugin.getName(),
parentPlugin);
}
} catch (SAXException e) {
GrailsCoreActivator.log(e);
} catch (IOException e) {
GrailsCoreActivator.log(e);
}
}
/**
* Creates a version model from the parent node, but does not add it to the
* list of plugins for the container plugin model yet.
*
* @param pluginNode
* @return
*/
protected PluginVersion createLatestReleasedVersionFromParentNode(
Node pluginNode) {
if (pluginNode == null || !PLUGIN_NODE.equals(pluginNode.getNodeName())) {
return null;
}
Node nameNode = pluginNode.getAttributes().getNamedItem(NAME_ATT);
Node latestReleaseNode = pluginNode.getAttributes().getNamedItem(
LATEST_RELEASE_ATT);
if (nameNode == null) {
return null;
}
String name = nameNode.getTextContent();
// plugin data must contain a plugin name
if (name == null) {
return null;
}
PluginVersion latestVersion = new PluginVersion();
latestVersion.setName(name);
String versionID = null;
if (latestReleaseNode != null) {
versionID = latestReleaseNode.getTextContent();
}
setVersionID(latestVersion, versionID);
return latestVersion;
}
/**
* Add a version extracted from given node, and return it if succesfully
* added, or return null if the version was not added (either invalid node,
* or version already exists). It also updates an existing version if the
* plugin model already contains the version.
* <p>
* Note that the latest version must be created first as it is the parent
* node in the XML structure. The latest version is therefore added to the
* list of versions ONLY when the corresponding child version is
* encountered, and additional information about the latest version is
* parsed.
* </p>
*
* @param releaseNode
* must not be null
* @param parentPlugin
* must not be null
* @param latestReleasedVersion
* must not be null. It is the parent node which always lists the
* latest version
* @return added version, or null if nothing is added
*/
protected PluginVersion addVersion(Node releaseNode,
GrailsPlugin parentPlugin, PluginVersion latestReleasedVersion) {
if (releaseNode == null || parentPlugin == null
|| !RELEASE_NODE.equals(releaseNode.getNodeName())
|| latestReleasedVersion == null) {
return null;
}
String versionID = null;
Node att = releaseNode.getAttributes().getNamedItem(VERSION_ATT);
if (att != null) {
versionID = att.getTextContent();
}
// check if an update is occuring
PluginVersion versionToAdd = parentPlugin.getVersion(versionID);
boolean isLatestVersionBeingAdded = latestReleasedVersion.getVersion()
.equals(versionID);
if (versionToAdd == null) {
// Check if the latest version has been encountered in the list of
// children. If so
// use the latest version instead of creating a new version.
if (isLatestVersionBeingAdded) {
versionToAdd = latestReleasedVersion;
} else {
versionToAdd = new PluginVersion();
setVersionID(versionToAdd, versionID);
}
addVersion(versionToAdd, parentPlugin);
}
NodeList list = releaseNode.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
Node child = list.item(i);
String name = child.getNodeName();
String value = child.getTextContent();
if (TITLE_NODE.equals(name)) {
versionToAdd.setTitle(value);
} else if (AUTHOR_NODE.equals(name)) {
versionToAdd.setAuthor(value);
} else if (DESCRIPTION_NODE.equals(name)) {
versionToAdd.setDescription(reformatDescription(value));
} else if (DOCUMENTATION_NODE.equals(name)) {
versionToAdd.setDocumentation(value);
}
}
return versionToAdd;
}
protected void setVersionID(PluginVersion version, String versionID) {
version.setVersion(versionID != null ? versionID : NO_VERSION_ID);
}
/**
* Adds the given version at the end of the list of plugins for the parent
* plugin, and returns true if successfully added. It also creates
* relationships between the version and the parent. Returns false if
* version was not added (either version is null, parent is null, or version
* already exists in the list)
*
* @param version
* @param parent
* @return true if successfully added. False otherwise
*/
protected boolean addVersion(PluginVersion version, GrailsPlugin parent) {
if (version == null || parent == null
|| parent.getVersions().contains(version)) {
return false;
}
version.setName(parent.getName());
return parent.addVersion(version);
}
public static boolean equals(String versionID1, String versionID2) {
if (versionID1 != null) {
return versionID1.equals(versionID2);
} else if (versionID2 != null) {
return false;
} else {
return true;
}
}
protected String reformatDescription(String description) {
if (description == null) {
return null;
}
description = description.trim();
StringBuffer descriptionBuffer = new StringBuffer(description);
for (; descriptionBuffer.length() > 0;) {
char charVal = description.charAt(0);
if (charVal == '\\' || Character.isWhitespace(charVal)) {
descriptionBuffer.deleteCharAt(0);
} else {
break;
}
}
description = descriptionBuffer.toString();
return description;
}
}