/*
* Copyright 2013 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Eclipse Public License version 1.0, available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jboss.forge.shell.plugins;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import org.apache.maven.model.Model;
import org.jboss.forge.ForgeEnvironment;
import org.jboss.forge.maven.MavenCoreFacet;
import org.jboss.forge.parser.ParserException;
import org.jboss.forge.parser.java.util.Strings;
import org.jboss.forge.parser.xml.Node;
import org.jboss.forge.parser.xml.XMLParser;
import org.jboss.forge.project.Project;
import org.jboss.forge.project.dependencies.Dependency;
import org.jboss.forge.project.dependencies.DependencyBuilder;
import org.jboss.forge.project.dependencies.DependencyResolver;
import org.jboss.forge.project.dependencies.ScopeType;
import org.jboss.forge.project.facets.DependencyFacet;
import org.jboss.forge.project.facets.MetadataFacet;
import org.jboss.forge.project.facets.PackagingFacet;
import org.jboss.forge.project.packaging.PackagingType;
import org.jboss.forge.project.services.ProjectFactory;
import org.jboss.forge.resources.DependencyResource;
import org.jboss.forge.resources.DirectoryResource;
import org.jboss.forge.resources.FileResource;
import org.jboss.forge.resources.Resource;
import org.jboss.forge.shell.InstalledPluginRegistry;
import org.jboss.forge.shell.PluginEntry;
import org.jboss.forge.shell.Shell;
import org.jboss.forge.shell.ShellMessages;
import org.jboss.forge.shell.events.PluginInstalled;
import org.jboss.forge.shell.events.PluginRemoved;
import org.jboss.forge.shell.exceptions.Abort;
/**
* Manages the installation and removal of plugins
*
* @author <a href="mailto:ggastald@redhat.com">George Gastaldi</a>
*
*/
public class PluginManager
{
private static final String MODULE_TEMPLATE_XML = "/org/jboss/forge/modules/module-template.xml";
private static final Dependency SHELL_API = DependencyBuilder.create("org.jboss.forge:forge-shell-api");
@Inject
private Event<PluginInstalled> pluginInstalledEvent;
@Inject
private Event<PluginRemoved> pluginRemovedEvent;
@Inject
private ForgeEnvironment environment;
@Inject
private Shell shell;
@Inject
private DependencyResolver resolver;
@Inject
private ProjectFactory projectFactory;
/**
* Called when a plugin needs to be removed
*
* @param pluginName
* @return
*/
public boolean removePlugin(final String pluginName)
{
PluginEntry plugin = PluginEntry.fromCoordinates(pluginName);
if (!InstalledPluginRegistry.has(plugin))
{
throw new RuntimeException("No such installed plugin [" + pluginName + "]");
}
PluginEntry installedPlugin = InstalledPluginRegistry.get(plugin);
InstalledPluginRegistry.remove(installedPlugin);
pluginRemovedEvent.fire(new PluginRemoved(installedPlugin));
return (!InstalledPluginRegistry.has(plugin));
}
/**
* Installs from a specific project
*
* @param buildDir
* @param coordinates The maven coordinates information of the required plugin
* @throws Abort
*/
public void installFromProject(final DirectoryResource buildDir, final Dependency coordinates) throws Abort
{
DirectoryResource root = projectFactory.findProjectRootRecusively(buildDir);
Project rootProject = projectFactory.findProject(root);
if (rootProject == null)
{
throw new IllegalArgumentException("Unable to recognise plugin project in ["
+ buildDir.getFullyQualifiedName() + "]");
}
Project pluginProject = findPluginProject(rootProject, coordinates);
if (pluginProject == null)
{
throw new Abort("The project does not contain a valid Forge Plugin project. Installation aborted");
}
DependencyFacet deps = pluginProject.getFacet(DependencyFacet.class);
String apiVersion = null;
Dependency directDependency = deps.getDirectDependency(SHELL_API);
if ((directDependency != null) && !Strings.isNullOrEmpty(directDependency.getVersion()))
apiVersion = directDependency.getVersion();
if (apiVersion == null)
{
// Fall back to checking managed dependencies for a version
Dependency managedDependency = deps.getManagedDependency(SHELL_API);
if ((managedDependency != null) && !Strings.isNullOrEmpty(managedDependency.getVersion()))
apiVersion = managedDependency.getVersion();
}
if (apiVersion == null)
{
// Now completely give up and just use the result from the build
Dependency effectiveDependency = deps.getEffectiveDependency(SHELL_API);
if (effectiveDependency != null)
apiVersion = effectiveDependency.getVersion();
else
apiVersion = environment.getRuntimeVersion();
}
/**
* Make sure that our PROVIDED modules are not included in the module dependencies
*/
// TODO Weld bug requires us to correct /add module for Seam Render dependency
List<String> groupIds = Arrays.asList("org.jboss.seam.render", "org.jboss.forge");
List<String> providedDeps = Arrays.asList("forge-javaee-api", "forge-maven-api", "forge-scaffold-api",
"forge-scaffoldx-api", "forge-shell-api");
List<Dependency> dependencies = deps.getDependencies();
for (Dependency dependency : dependencies)
{
if (groupIds.contains(dependency.getGroupId())
&& !(ScopeType.PROVIDED.equals(dependency.getScopeTypeEnum())
|| ScopeType.TEST.equals(dependency.getScopeTypeEnum())))
{
ShellMessages.warn(shell, "Dependency [" + dependency.toCoordinates()
+ "] was not correctly marked as PROVIDED scope; this has been corrected.");
deps.addDirectDependency(DependencyBuilder.create(dependency).setScopeType(ScopeType.PROVIDED));
}
else if (dependency.getGroupId().equals("org.jboss.forge")
&& !providedDeps.contains(dependency.getArtifactId())
&& !ScopeType.TEST.equals(deps.getEffectiveDependency(dependency).getScopeTypeEnum()))
{
ShellMessages.warn(shell,
"Plugin has a dependency on internal Forge API [" + dependency
+ "] - this is not allowed and may cause failures.");
}
}
ShellMessages.info(shell, "Invoking build with underlying build system.");
// Build the whole project
// Inter-module dependencies are not resolved when mvn package is just run. Perhaps it should install ?
Resource<?> artifact;
if (rootProject.equals(pluginProject))
{
artifact = rootProject.getFacet(PackagingFacet.class).createBuilder().runTests(false).build();
}
else
{
rootProject.getFacet(PackagingFacet.class).createBuilder().addArguments("clean", "install").runTests(false).build();
artifact = pluginProject.getFacet(PackagingFacet.class).getFinalArtifact();
}
// However, get only the necessary plugin artifact
if ((artifact != null) && artifact.exists())
{
MetadataFacet meta = rootProject.getFacet(MetadataFacet.class);
Dependency dep = meta.getOutputDependency();
ShellMessages.info(shell, "Installing plugin artifact.");
// TODO Figure out a better plugin versioning strategy than random numbers, also see if unloading is
// possible to avoid this entirely.
createModule(
pluginProject,
DependencyBuilder.create(dep).setVersion(
dep.getVersion() + "-" + UUID.randomUUID().toString()),
artifact, apiVersion);
}
else
{
throw new IllegalStateException("Build artifact [" + artifact
+ "] is missing and cannot be installed. Please resolve build errors and try again.");
}
}
/**
* This method will try its best to find the plugin coordinates for a given project
*
* @param project
* @return
*/
Project findPluginProject(final Project rootProject, final Dependency coordinates)
{
final Project pluginProject;
Model rootPom = rootProject.getFacet(MavenCoreFacet.class).getPOM();
Dependency projectDependency = DependencyBuilder.create()
.setGroupId(rootPom.getGroupId() == null ? rootPom.getParent().getGroupId() : rootPom.getGroupId())
.setArtifactId(rootPom.getArtifactId()).setPackagingType(rootPom.getPackaging());
// 1) Check if the root project is a multimodule project
if (projectDependency.getPackagingTypeEnum() == PackagingType.BASIC)
{
Project tempPluginProject = null;
DirectoryResource projectDir = rootProject.getProjectRoot();
for (String module : rootPom.getModules())
{
DirectoryResource moduleDir = projectDir.getChildDirectory(module);
// 1.1) If yes, traverse the projects
Project moduleProject = projectFactory.findProject(moduleDir);
// If module is empty, avoid infinite recursion
if (moduleProject.equals(rootProject))
{
continue;
}
tempPluginProject = findPluginProject(moduleProject, coordinates);
if (tempPluginProject != null)
{
break;
}
}
pluginProject = tempPluginProject;
}
else
{
// 1.2) If not, check if the provided coordinate (if not null) matches the requested project
// If no coordinates were provided or if the coordinate matches the current POM, return the the root project is
// the plugin project
if (coordinates == null || DependencyBuilder.areEquivalent(projectDependency, coordinates))
{
pluginProject = rootProject;
}
else
{
pluginProject = null;
}
}
return pluginProject;
}
private boolean needDependenciesAsResourceRoot(final Project project)
{
FileResource<?> forgeXml = (FileResource<?>) project.getProjectRoot().getChild(
"src/main/resources/META-INF/forge.xml");
if (forgeXml.exists())
{
try
{
Node node = XMLParser.parse(forgeXml.getResourceInputStream());
return node.getSingle("dependencies-as-resource-root") != null;
}
catch (ParserException e)
{
return false;
}
}
return false;
}
private DirectoryResource createModule(final Project project, final Dependency dep, final Resource<?> artifact,
final String apiVersion) throws Abort
{
boolean dependenciesAsResourceRoot = needDependenciesAsResourceRoot(project);
DirectoryResource moduleDir = getOrCreatePluginModuleDirectory(dep);
String pluginName = dep.getGroupId() + "." + dep.getArtifactId();
String pluginSlot = dep.getVersion();
FileResource<?> moduleXml = (FileResource<?>) moduleDir.getChild("module.xml");
if (moduleXml.exists()
&& !shell.promptBoolean(
"An existing installation for version [" + pluginSlot
+ "] of this plugin was found. Replace it?", true))
{
throw new Abort("Aborted.");
}
moduleXml.delete();
moduleXml.createNewFile();
// <resource-root path="maven-dependency.jar" />
Node module = XMLParser.parse(getClass().getResourceAsStream(MODULE_TEMPLATE_XML));
module.attribute("name", pluginName);
module.attribute("slot", pluginSlot);
Node resources = module.getSingle("resources");
resources.createChild("resource-root").attribute("path", dep.getArtifactId() + ".jar");
if (dependenciesAsResourceRoot)
{
writeResourceRoots(project, module, moduleDir, resources);
}
// Copy the compiled JAR into the module directory
FileResource<?> jar = moduleDir.getChild(dep.getArtifactId() + ".jar").reify(FileResource.class);
jar.createNewFile();
jar.setContents(artifact.getResourceInputStream());
// <module name="org.jboss.forge:main" />
Node dependencies = module.getSingle("dependencies");
if (!dependenciesAsResourceRoot)
{
dependencies.createChild("module").attribute("name", pluginName + ".dependencies")
.attribute("slot", pluginSlot);
}
dependencies.createChild("module").attribute("name", "org.jboss.forge.javaee.api")
.attribute("services", "import");
dependencies.createChild("module").attribute("name", "org.jboss.forge.maven.api")
.attribute("services", "import");
dependencies.createChild("module").attribute("name", "org.jboss.forge.scaffold.api")
.attribute("services", "import");
dependencies.createChild("module").attribute("name", "org.jboss.forge.scaffoldx.api")
.attribute("services", "import");
dependencies.createChild("module").attribute("name", "org.jboss.forge.shell.api")
.attribute("services", "import");
dependencies.createChild("module").attribute("name", "org.jboss.seam.render").attribute("services", "import");
dependencies.createChild("module").attribute("name", "javax.api");
moduleXml.setContents(XMLParser.toXMLString(module));
if (!dependenciesAsResourceRoot)
{
createDependenciesModule(project, dep);
}
// Add to list modules.
registerPlugin(pluginName, pluginSlot, apiVersion);
return moduleDir;
}
private List<DependencyResource> getPluginDependencies(final Project project, Node module)
{
DependencyFacet deps = project.getFacet(DependencyFacet.class);
List<DependencyResource> pluginDependencies = new ArrayList<DependencyResource>();
// Hack to invalidate the cached full project build result in the MavenCoreFacet
// Thie ensures that resolution of any unresolved dependencies in a multi-module project will be re-attempted.
MavenCoreFacet mvn = project.getFacet(MavenCoreFacet.class);
mvn.setPOM(mvn.getPOM());
List<Dependency> effectiveDependenciesInScopes = deps.getEffectiveDependenciesInScopes(ScopeType.COMPILE,
ScopeType.RUNTIME);
for (Dependency d : effectiveDependenciesInScopes)
{
if (d.getPackagingTypeEnum().equals(PackagingType.JAR)
&& !d.getGroupId().equals("org.jboss.forge"))
{
List<DependencyResource> artifacts = resolveArtifacts(project, d);
pluginDependencies.addAll(artifacts);
}
// TODO encapsulate this?
if (DependencyBuilder.areEquivalent(d, DependencyBuilder.create("org.jboss.forge:forge-javaee-api")))
{
module.getSingle("dependencies").createChild("module")
.attribute("name", "org.jboss.forge.javaee.api")
.attribute("services", "import");
}
else if (DependencyBuilder.areEquivalent(d, DependencyBuilder.create("org.jboss.forge:forge-scaffold-api")))
{
module.getSingle("dependencies").createChild("module")
.attribute("name", "org.jboss.forge.scaffold.api")
.attribute("services", "import");
}
else if (DependencyBuilder.areEquivalent(d, DependencyBuilder.create("org.jboss.forge:forge-scaffoldx-api")))
{
module.getSingle("dependencies").createChild("module")
.attribute("name", "org.jboss.forge.scaffoldx.api")
.attribute("services", "import");
}
else if (DependencyBuilder.areEquivalent(d, DependencyBuilder.create("org.jboss.forge:forge-maven-api")))
{
module.getSingle("dependencies").createChild("module")
.attribute("name", "org.jboss.forge.maven.api")
.attribute("services", "import");
}
else if (d.getGroupId().equals("org.jboss.forge"))
{
ShellMessages.error(shell,
"Plugin has a dependency on internal Forge API [" + d
+ "] - this is not allowed and may cause failures.");
}
}
return pluginDependencies;
}
private void writeResourceRoots(
final Project project,
final Node module,
final DirectoryResource directory,
final Node resources)
{
List<DependencyResource> pluginDependencies = getPluginDependencies(project, module);
for (DependencyResource d : pluginDependencies)
{
String name = d.getName();
Resource<?> child = directory.getChild(name);
child.delete();
FileResource<?> depJar = child.reify(FileResource.class);
depJar.setContents(d.getResourceInputStream());
resources.createChild("resource-root").attribute("path", name);
}
}
private void createDependenciesModule(final Project project, final Dependency dep)
{
DirectoryResource dependencyDir = getOrCreatePluginDependenciesModuleDirectory(dep);
String pluginName = dep.getGroupId() + "." + dep.getArtifactId();
String pluginSlot = dep.getVersion();
FileResource<?> moduleXml = (FileResource<?>) dependencyDir.getChild("module.xml");
moduleXml.delete();
moduleXml.createNewFile();
// <resource-root path="maven-dependency.jar" />
Node module = XMLParser.parse(getClass().getResourceAsStream(MODULE_TEMPLATE_XML));
module.attribute("name", pluginName + ".dependencies");
module.attribute("slot", pluginSlot);
Node resources = module.getSingle("resources");
// <module name="org.jboss.forge:main" />
Node dependencies = module.getSingle("dependencies");
dependencies.createChild("module").attribute("name", "javax.api");
dependencies.createChild("module").attribute("name", "org.jboss.forge.shell.api");
writeResourceRoots(project, module, dependencyDir, resources);
// Write out the module XML file.
moduleXml.setContents(XMLParser.toXMLString(module));
}
private List<DependencyResource> resolveArtifacts(final Project project, final Dependency dep)
{
Dependency d = dep;
List<DependencyResource> artifacts = new ArrayList<DependencyResource>();
DependencyFacet deps = project.getFacet(DependencyFacet.class);
for (Dependency d2 : deps.getDependencies())
{
if (DependencyBuilder.areEquivalent(d, d2) && (d2.getVersion() != null))
{
d = d2;
break;
}
}
if (artifacts.size() != 1)
{
artifacts = resolver.resolveArtifacts(d, deps.getRepositories());
}
if (artifacts.size() != 1)
{
ShellMessages.warn(shell, "Could not resolve dependency [" + d.toCoordinates() + "]");
}
return artifacts;
}
private void registerPlugin(final String pluginName, final String pluginSlot, final String apiVersion)
{
String runtimeVersion = InstalledPluginRegistry.getRuntimeAPIVersion();
if (InstalledPluginRegistry.isApiCompatible(runtimeVersion, apiVersion))
{
PluginEntry entry = InstalledPluginRegistry.install(pluginName, apiVersion, pluginSlot);
pluginInstalledEvent.fire(new PluginInstalled(entry));
}
else
{
throw new RuntimeException(
"Could not install plugin ["
+ pluginName
+ "] because it references Forge API version ["
+ apiVersion
+ "] which may not be compatible with my current version ["
+ runtimeVersion
+ "]. Please consider upgrading forge, by typing 'forge update'. Otherwise, try installing an older version of the plugin.");
}
}
private DirectoryResource getOrCreatePluginModuleDirectory(final Dependency dep)
{
DirectoryResource pluginDir = environment.getPluginDirectory();
List<String> groupId = Arrays.asList(dep.getGroupId().split("\\."));
List<String> artifactId = Arrays.asList(dep.getArtifactId().split("\\."));
DirectoryResource dir = pluginDir;
for (String segment : groupId)
{
dir = dir.getOrCreateChildDirectory(segment);
}
for (String segment : artifactId)
{
dir = dir.getOrCreateChildDirectory(segment);
}
dir = dir.getOrCreateChildDirectory(dep.getVersion());
return dir;
}
private DirectoryResource getOrCreatePluginDependenciesModuleDirectory(final Dependency dep)
{
DirectoryResource pluginDir = environment.getPluginDirectory();
List<String> groupId = Arrays.asList(dep.getGroupId().split("\\."));
List<String> artifactId = Arrays.asList(dep.getArtifactId().split("\\."));
DirectoryResource dir = pluginDir;
for (String segment : groupId)
{
dir = dir.getOrCreateChildDirectory(segment);
}
for (String segment : artifactId)
{
dir = dir.getOrCreateChildDirectory(segment);
}
dir = dir.getOrCreateChildDirectory("dependencies");
dir = dir.getOrCreateChildDirectory(dep.getVersion());
return dir;
}
}