/*
* RHQ Management Platform
* Copyright (C) 2005-2014 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.bundle.ant;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.MagicNames;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.UnknownElement;
import org.apache.tools.ant.helper.AntXMLContext;
import org.apache.tools.ant.helper.ProjectHelper2;
import org.rhq.bundle.ant.task.BundleTask;
import org.rhq.bundle.ant.type.DeploymentUnitType;
/**
* This object enables you to invoke an Ant script within the running VM. You can fully run the script
* or you can ask that the script just be parsed and validated but no tasks executed.
*
* @author John Mazzitelli
* @author Ian Springer
*/
public class AntLauncher {
private static final Log LOG = LogFactory.getLog(AntLauncher.class);
// "out of box" we will provide the ant contrib optional tasks (from ant-contrib.jar)
private static final String ANTCONTRIB_ANT_TASKS = "net/sf/antcontrib/antcontrib.properties";
// "out of box" we will provide the liquibase tasks (from liquibase-core.jar)
private static final String LIQUIBASE_ANT_TASKS = "liquibasetasks.properties";
// private constant ProjectHelper2.REFID_CONTEXT value
private static final String REFID_CONTEXT = "ant.parsing.context";
private static final String BUNDLE_TASK_NAME = "rhq:bundle";
private boolean requireExplicitCompliance;
private HandoverTarget handoverTarget;
/**
* For backwards compatibility reasons, this calls {@link #AntLauncher(boolean) AntLauncher(false)}.
* Note that this might change in the future, because we are <b>requiring</b> the explicit declaration of the
* destination directory's compliance mode starting with RHQ 4.9.0.
* <p/>
* Nevertheless this constructor is behaving as it was before RHQ 4.9.0 so that users of it aren't surprised
* by its behavior.
*
* @deprecated since 4.9.0. You can keep using this constructor but be aware that it might change behavior in some
* future version of RHQ. It will NOT be removed though.
*/
@Deprecated
public AntLauncher() {
this(false);
}
/**
* @since 4.9.0
* @param requireExplicitCompliance whether or not to enforce the presence of {@code compliance} attribute in the
* deployment unit definitions. Before RHQ 4.9.0 a similar deprecated attribute
* called {@code manageRootDir} was optional and defaulted to {@code true}. Since
* RHQ 4.9.0 we require the user to explicitly specify the compliance of the
* destination directory. But to keep backwards compatibility with the older
* bundle recipes already deployed on the agents, we make this behavior optional.
*/
public AntLauncher(boolean requireExplicitCompliance) {
this.requireExplicitCompliance = requireExplicitCompliance;
}
public void setHandoverTarget(HandoverTarget handoverTarget) {
this.handoverTarget = handoverTarget;
}
/**
* Executes the specified bundle deploy Ant build file (i.e. rhq-deploy.xml).
*
* @param buildFile the path to the build file (i.e. rhq-deploy.xml)
* @param buildProperties the properties to pass into Ant
* @param buildListeners a list of build listeners (provide callback methods for targetExecuted, taskExecuted, etc.)
*
* @return the bundle Ant project containing information about the specified build file
*
* @throws InvalidBuildFileException if the build file is invalid
*/
public BundleAntProject executeBundleDeployFile(File buildFile, Properties buildProperties,
List<BuildListener> buildListeners) throws InvalidBuildFileException {
// Parse and validate the build file before even attempting to execute it.
BundleAntProject parsedProject = parseBundleDeployFile(buildFile, buildProperties);
BundleAntProject project = createProject(buildFile, false, buildProperties);
if (handoverTarget != null) {
project.setHandoverTarget(handoverTarget);
}
// The parse above got us all the bundle files names. The rest of this method
// will be able to re-determine everything else for 'project' but these filenames.
// Therefore, we need to copy those filenames from the parsedProject to project.
// The rest of project will be filled in later.
project.getBundleFileNames().addAll(parsedProject.getBundleFileNames());
try {
if (buildListeners != null) {
for (BuildListener buildListener : buildListeners) {
project.addBuildListener(buildListener);
}
}
// Add taskdefs for the Ant tasks that we bundle, so user won't have to explicitly declare them in their
// build file.
addTaskDefsForBundledTasks(project);
// This will parse the build file and initialize all tasks, as well as execute the implicit target,
// which contains the rhq:bundle task.
ProjectHelper.configureProject(project, buildFile);
return project;
} catch (Exception e) {
throw new RuntimeException("Failed to execute bundle deploy file [" + buildFile.getAbsolutePath()
+ "]. Cause: " + e, e);
}
}
public BundleAntProject parseBundleDeployFile(File buildFile, Properties buildProperties)
throws InvalidBuildFileException {
BundleAntProject project = createProject(buildFile, true, buildProperties);
if (handoverTarget != null) {
project.setHandoverTarget(handoverTarget);
}
ProjectHelper2 projectHelper = new ProjectHelper2();
try {
// Use the 3-param version of parse(), rather than the 2-param version, or ProjectHelper.configureProject(),
// to avoid actually executing the implicit target (which would cause the rhq:bundle task to be executed).
AntXMLContext context = (AntXMLContext) project.getReference(REFID_CONTEXT);
projectHelper.parse(project, buildFile, new ProjectHelper2.RootHandler(context,
new ProjectHelper2.MainHandler()));
} catch (BuildException e) {
throw new InvalidBuildFileException("Failed to parse bundle Ant build file.", e);
}
validateAndPreprocess(project);
if (LOG.isDebugEnabled()) {
LOG.debug("==================== PARSED BUNDLE ANT BUILD FILE ====================");
LOG.debug(" Bundle Name: " + project.getBundleName());
LOG.debug(" Bundle Version: " + project.getBundleVersion());
LOG.debug(" Bundle Description: " + project.getBundleDescription());
LOG.debug(" Deployment Config Def: "
+ project.getConfigurationDefinition().getPropertyDefinitions().values());
LOG.debug("======================================================================");
}
return project;
}
private BundleAntProject createProject(File buildFile, boolean parseOnly, Properties buildProperties) {
ClassLoader classLoader = getClass().getClassLoader();
BundleAntProject project = new BundleAntProject(parseOnly);
if (buildProperties != null) {
for (Map.Entry<Object, Object> property : buildProperties.entrySet()) {
// On the assumption that these properties will be slurped in via Properties.load we
// need to escape backslashes to have them treated as literals
project.setProperty(property.getKey().toString(), property.getValue().toString().replace("\\", "\\\\"));
}
}
project.setProperty(MagicNames.ANT_FILE, buildFile.getAbsolutePath());
project.setProperty(MagicNames.ANT_FILE_TYPE, MagicNames.ANT_FILE_TYPE_FILE);
project.setCoreLoader(classLoader);
project.init();
project.setBaseDir(buildFile.getParentFile());
AntXMLContext context = new AntXMLContext(project);
context.setCurrentTargets(new HashMap());
project.addReference(REFID_CONTEXT, context);
project.addReference(ProjectHelper2.REFID_TARGETS, context.getTargets());
return project;
}
private void addTaskDefsForBundledTasks(BundleAntProject project) throws IOException, ClassNotFoundException {
Properties taskDefs = buildTaskDefProperties(project.getCoreLoader());
for (Map.Entry<Object, Object> taskDef : taskDefs.entrySet()) {
project.addTaskDefinition(taskDef.getKey().toString(),
Class.forName(taskDef.getValue().toString(), true, project.getCoreLoader()));
}
}
private Properties buildTaskDefProperties(ClassLoader classLoader) throws IOException {
Set<String> customTaskDefs = new HashSet<String>(2);
customTaskDefs.add(ANTCONTRIB_ANT_TASKS);
customTaskDefs.add(LIQUIBASE_ANT_TASKS);
Properties taskDefProps = new Properties();
for (String customTaskDef : customTaskDefs) {
InputStream taskDefsStream = classLoader.getResourceAsStream(customTaskDef);
if (taskDefsStream != null) {
try {
taskDefProps.load(taskDefsStream);
} catch (Exception e) {
LOG.warn("Ant task definitions [" + customTaskDef
+ "] failed to load - ant bundles cannot use their tasks", e);
} finally {
taskDefsStream.close();
}
} else {
LOG.warn("Missing ant task definitions [" + customTaskDef + "] - ant bundles cannot use their tasks");
}
}
return taskDefProps;
}
private void validateAndPreprocess(BundleAntProject project) throws InvalidBuildFileException {
AntXMLContext antParsingContext = (AntXMLContext) project.getReference("ant.parsing.context");
Vector targets = antParsingContext.getTargets();
int bundleTaskCount = 0;
Task unconfiguredBundleTask = null;
for (Object targetObj : targets) {
Target target = (Target) targetObj;
Task[] tasks = target.getTasks();
for (Task task : tasks) {
if (task.getTaskName().equals(BUNDLE_TASK_NAME)) {
abortIfTaskWithinTarget(target, task);
bundleTaskCount++;
unconfiguredBundleTask = task;
}
}
}
if (bundleTaskCount == 0) {
throw new InvalidBuildFileException(
"rhq:bundle task not found - an RHQ bundle Ant build file must contain exactly one rhq:bundle task.");
}
if (bundleTaskCount > 1) {
throw new InvalidBuildFileException(
"More than one rhq:bundle task found - an RHQ bundle Ant build file must contain exactly one rhq:bundle task.");
}
BundleTask bundleTask = (BundleTask) preconfigureTask(unconfiguredBundleTask);
Collection<DeploymentUnitType> deployments = bundleTask.getDeploymentUnits().values();
if (deployments.isEmpty()) {
throw new InvalidBuildFileException(
"The bundle task must contain exactly one rhq:deploymentUnit child element.");
}
DeploymentUnitType deployment = deployments.iterator().next();
if (requireExplicitCompliance && deployment.getCompliance() == null) {
throw new InvalidBuildFileException(
"The deployment unit must specifically declare compliance mode of the destination directory.");
}
project.setDestinationCompliance(deployment.getCompliance());
Map<File, String> files = deployment.getLocalFileNames();
for (String file : files.values()) {
project.getBundleFileNames().add(file);
}
Map<File, String> archives = deployment.getLocalArchiveNames();
for (String archive : archives.values()) {
project.getBundleFileNames().add(archive);
}
List<String> propertyFiles = bundleTask.getLocalPropertyFiles();
for (String propFile : propertyFiles) {
project.getBundleFileNames().add(propFile);
}
// note that we do NOT add url-files and url-archives to the BundleFileNames because those are
// not true "bundle files" that are stored with the bundle version in the database. Those will
// be downloaded by the agents at the time the recipe is invoked. There is nothing server side
// that need to be known about the files/archives from URLs.
return;
}
private void abortIfTaskWithinTarget(Target target, Task task) throws InvalidBuildFileException {
if (!target.getName().equals("")) {
throw new InvalidBuildFileException(task.getTaskName() + " task found within [" + target.getName()
+ "] target - it must be outside of any targets (at the top of the build file).");
}
}
private static Task preconfigureTask(Task task) {
if (task instanceof UnknownElement) {
task.maybeConfigure();
Task resolvedTask = ((UnknownElement) task).getTask();
return (resolvedTask != null) ? resolvedTask : task;
} else {
return task;
}
}
}