AntProject.java

// $Id$
/*
 * ====================================================================
 * Copyright (c) 2002-2004, Christophe Labouisse All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package net.ggtools.grand.ant;

import java.io.File;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import net.ggtools.grand.ant.taskhelpers.SubAntHelper;
import net.ggtools.grand.ant.taskhelpers.TaskDefHelper;
import net.ggtools.grand.exceptions.DuplicateElementException;
import net.ggtools.grand.exceptions.GrandException;
import net.ggtools.grand.graph.Graph;
import net.ggtools.grand.graph.GraphProducer;
import net.ggtools.grand.graph.Node;
import net.ggtools.grand.log.LoggerManager;

import org.apache.commons.logging.Log;
import org.apache.tools.ant.AntTypeDefinition;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.ComponentHelper;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.Task;

/**
 * A graph producer from Ant build files or {@link org.apache.tools.ant.Project}
 * objects. The nodes will be the project's target and the links will be the
 * dependencies between targets. Beside <i>hard</i> dependencies, this producer
 * is also able to create weak links from dependencies introduced by the use of
 * the <code>antcall</code> or <code>foreach</code> tasks.
 *
 * @author Christophe Labouisse
 * @see <a href="http://ant-contrib.sourceforge.net/">Ant contrib tasks</a> for
 *      the <code>foreach</code> task.
 */
public class AntProject implements GraphProducer {
    /**
     * Field log.
     */
    private static final Log LOG = LoggerManager.getLog(AntProject.class);

    /**
     * A condition helper using the {@link Target#getIf()} &amp;
     * {@link Target#getUnless()} methods introduced in Ant 1.6.2.
     * @author Christophe Labouisse
     */
    private static class GetterConditionHelper
        implements TargetConditionHelper {

        /**
         * Build a new GetterConditionHelper. Since the need methods are only
         * available in the last versions of Ant, we check if those method can
         * be loaded.
         * @throws NoSuchMethodException when methods are not found
         */
        private GetterConditionHelper() throws NoSuchMethodException {
            final Class<?>[] parameters = new Class[]{};
            Target.class.getMethod("getIf", parameters);
            Target.class.getMethod("getUnless", parameters);
        }

        /**
         * Method getIfCondition.
         * @param target Target
         * @return String
         * @see net.ggtools.grand.ant.AntProject.TargetConditionHelper#getIfCondition(org.apache.tools.ant.Target)
         */
        public String getIfCondition(final Target target) {
            return target.getIf();
        }

        /**
         * Method getUnlessCondition.
         * @param target Target
         * @return String
         * @see net.ggtools.grand.ant.AntProject.TargetConditionHelper#getUnlessCondition(org.apache.tools.ant.Target)
         */
        public String getUnlessCondition(final Target target) {
            return target.getUnless();
        }

    }

    /**
     * A condition helper always returning <code>null</code>. This class will
     * be used as a fallback helper
     *
     * @author Christophe Labouisse
     */
    private static class NullConditionHelper implements TargetConditionHelper {

        /**
         * Method getIfCondition.
         * @param target Target
         * @return String
         * @see net.ggtools.grand.ant.AntProject.TargetConditionHelper#getIfCondition(org.apache.tools.ant.Target)
         */
        public String getIfCondition(final Target target) {
            return null;
        }

        /**
         * Method getUnlessCondition.
         * @param target Target
         * @return String
         * @see net.ggtools.grand.ant.AntProject.TargetConditionHelper#getUnlessCondition(org.apache.tools.ant.Target)
         */
        public String getUnlessCondition(final Target target) {
            return null;
        }

    }

    /**
     * A dirty hack using {@link Field} methods in order to gain access to the
     * private {@link Target#ifCondition} and {@link Target#unlessCondition}
     * attributes.
     *
     * @author Christophe Labouisse
     */
    private static class ReflectHelper implements TargetConditionHelper {
        /**
         * Field ifCondition.
         */
        private final Field ifCondition;

        /**
         * Field unlessCondition.
         */
        private final Field unlessCondition;

        /**
         * Constructor for ReflectHelper.
         * @throws NoSuchFieldException when fields are not found
         */
        private ReflectHelper() throws NoSuchFieldException {
            ifCondition = Target.class.getDeclaredField("ifCondition");
            unlessCondition = Target.class.getDeclaredField("unlessCondition");
            AccessibleObject.setAccessible(new AccessibleObject[]{ifCondition, unlessCondition}, true);
        }

        /**
         * Method getIfCondition.
         * @param target Target
         * @return String
         * @see net.ggtools.grand.ant.AntProject.TargetConditionHelper#getIfCondition(org.apache.tools.ant.Target)
         */
        public String getIfCondition(final Target target) {
            String result = null;

            try {
                result = (String) ifCondition.get(target);
                if (result != null && result.isEmpty()) {
                    result = null;
                }
            } catch (final Exception e) {
                LOG.error("Caught exception, ignoring if condition", e);
            }

            return result;
        }

        /**
         * Method getUnlessCondition.
         * @param target Target
         * @return String
         * @see net.ggtools.grand.ant.AntProject.TargetConditionHelper#getUnlessCondition(org.apache.tools.ant.Target)
         */
        public String getUnlessCondition(final Target target) {
            String result = null;

            try {
                result = (String) unlessCondition.get(target);
                if (result != null && result.isEmpty()) {
                    result = null;
                }
            } catch (final Exception e) {
                LOG.error("Caught exception, ignoring unless condition", e);
            }

            return result;
        }

    }

    /**
     * Helper interface to access the <em>if</em> and <em>unless</em>
     * conditions of targets.
     *
     * @author Christophe Labouisse
     */
    private interface TargetConditionHelper {
        /**
         * Returns the <em>if condition</em> for a specific target.
         *
         * @param target Target
         * @return the <em>if condition</em> or <code>null</code> if none
         *         defined.
         */
        String getIfCondition(final Target target);

        /**
         * Returns the <em>unless condition</em> for a specific target.
         *
         * @param target Target
         * @return the <em>unless condition</em> or <code>null</code> if
         *         none defined.
         */
        String getUnlessCondition(final Target target);
    }

    /**
     * Factory class creating TargetConditionHelper objects.
     *
     * @author Christophe Labouisse
     */
    private static final class TargetConditionHelperFactory {

        /**
         * Creates a new TargetConditionHelper.
         *
         * @return the best help available.
         */
        public static TargetConditionHelper getTargetConditionHelper() {
            TargetConditionHelper result;

            try {
                result = new GetterConditionHelper();
                LOG.debug("Using Ant getter");
            } catch (final Exception e) {
                LOG.debug("Cannot create GetterConditionHelper, trying next one");
                result = null;
            }

            if (result == null) {
                try {
                    result = new ReflectHelper();
                    LOG.debug("Using ReflectHelper");
                } catch (final Exception e) {
                    LOG.debug("Cannot create ReflectHelper, trying next one");
                    result = null;
                }
            }

            if (result == null) {
                result = new NullConditionHelper();
            }

            return result;
        }

        /**
         * Constructor for TargetConditionHelperFactory.
         */
        private TargetConditionHelperFactory() {
        }
    }

    /**
     * Field antProject.
     */
    private final Project antProject;

    /**
     * Field targetConditionHelper.
     */
    private final TargetConditionHelper targetConditionHelper =
            TargetConditionHelperFactory.getTargetConditionHelper();

    /**
     * Field targetExplorer.
     */
    private final TargetTasksExplorer targetExplorer =
            new TargetTasksExplorer(this);

    /**
     * Field taskLinkFinder.
     */
    private LinkFinderVisitor taskLinkFinder;

    /**
     * Creates a new project from an Ant build file.
     *
     * The source object can be anything supported by {@link ProjectHelper}
     * which is at least a File.
     *
     * @param source
     *            The source for XML configuration.
     * @see ProjectHelper#parse(org.apache.tools.ant.Project, java.lang.Object)
     * @throws GrandException
     *             if the project cannot be loaded.
     */
    public AntProject(final File source) throws GrandException {
        this(source, null);
    }

    /**
     * Creates a new project from an Ant build file.
     *
     * The source object can be anything supported by {@link ProjectHelper}
     * which is at least a File.
     *
     * @param source
     *            The source for XML configuration.
     * @param properties
     *            a set of properties to be preset when opening the graph or
     *            <code>null</code> if no properties should be preset.
     * @see ProjectHelper#parse(org.apache.tools.ant.Project, java.lang.Object)
     * @throws GrandException
     *             if the project cannot be loaded.
     */
    public AntProject(final File source, final Properties properties)
            throws GrandException {
        LOG.info("Parsing from " + source);
        antProject = new Project();
        if (properties != null) {
            for (final Entry<Object, Object> element : properties.entrySet()) {
                antProject.setProperty((String) element.getKey(),
                        (String) element.getValue());
            }
        }
        antProject.setSystemProperties();
        antProject.init();
        antProject.setUserProperty("ant.file", source.getAbsolutePath());

        postInit();

        try {
            final ProjectHelper loader = ProjectHelper.getProjectHelper();
            antProject.addReference("ant.projectHelper", loader);
            loader.parse(antProject, source);
            LOG.debug("Done parsing");
        } catch (final BuildException e) {
            final String message = "Cannot open project file " + source;
            LOG.error(message, e);
            // TODO better rethrowing?
            throw new GrandException(message, e);
        }
    }

    /**
     * Perform common operations after the object building.
     */
    private void postInit() {
        taskLinkFinder = new LinkFinderVisitor(this);

        // Change the component helper to instantiate SubAntHelper for subant
        // task.
        final ComponentHelper helper = antProject.getReference("ant.ComponentHelper");
        if (helper != null) {
            final AntTypeDefinition subAntDef = helper.getDefinition("subant");
            if (subAntDef == null) {
                LOG.warn("No definition found for the subant task in ComponentHelper, disabling subant");
            } else {
                subAntDef.setClass(SubAntHelper.class);
            }
            final AntTypeDefinition taskDefDef = helper.getDefinition("taskdef");
            if (taskDefDef == null) {
                LOG.warn("No definition found for the taskdef task in ComponentHelper, some file may not load properly");
            } else {
                taskDefDef.setClass(TaskDefHelper.class);
            }
        } else {
            LOG.warn("No component helper in current project");
        }
    }

    /**
     * Creates a new project from an existing Ant project.
     *
     * @param project
     *            project to create the graph from.
     */
    public AntProject(final Project project) {
        antProject = project;
        postInit();
    }

    /**
     * Returns the underlying Ant project.
     *
     * @return underlying Ant project.
     */
    public final Project getAntProject() {
        return antProject;
    }

    /**
     * Convert an Ant project to a Grand Graph.
     *
     * The conversion is done in several steps:
     *
     * <ol>
     * <li>Each Ant target will be converted to a Grand Node using both the
     * target's name and description the the node's ones. Targets with a non
     * empty description will be converted to Nodes with the
     * {@link Node#ATTR_MAIN_NODE}attribute set. If the project element has a
     * valid default target, the corresponding node will be the graph start
     * target.</li>
     * <li><code>depends</code> attributes on targets will be translated into
     * links. <code>antcall</code> s or the contributed <code>foreach</code>
     * task will be translated in links with the
     * {@link net.ggtools.grand.graph.Link#ATTR_WEAK_LINK}set.</li>
     * </ol>
     *
     * @return a graph representing the dependency of the Ant project.
     * @throws GrandException
     *             if the project cannot be converted to a graph.
     * @see GraphProducer#getGraph()
     */
    public final Graph getGraph() throws GrandException {
        LOG.debug("Triggering AntProject");

        final AntGraph graph = new AntGraph(antProject);
        final Map<String, String> targetMap = new HashMap<String, String>();

        // First pass, create the nodes.
        for (final Target target : antProject.getTargets().values()) {
            if (target.getName().isEmpty()) {
                continue;
            }

            final String targetName = target.getName();
            final AntTargetNode node =
                    (AntTargetNode) graph.createNode(targetName);

            // Prefixed nodes have the same location as non-prefixed; skip them.
            String location = target.getLocation().toString();
            if (!targetMap.containsKey(location)
                    || targetMap.get(location).length() > targetName.length()) {
                targetMap.put(location, targetName);
            }

            // Mark nodes with a description as MAIN.
            final String targetDescription = target.getDescription();
            if (targetDescription != null && !targetDescription.isEmpty()) {
                node.setAttributes(Node.ATTR_MAIN_NODE);
                node.setDescription(targetDescription);
            }

            node.setIfCondition(targetConditionHelper.getIfCondition(target));
            node.setUnlessCondition(targetConditionHelper.getUnlessCondition(target));
            targetExplorer.exploreTarget(node, target);
        }

        // Sets the start node if needed.
        final String defaultTarget = antProject.getDefaultTarget();
        if (defaultTarget != null) {
            final Node startNode = graph.getNode(defaultTarget);

            if (startNode != null) {
                graph.setStartNode(startNode);
            }
        }

        // Second pass, create the links
        for (final Target target : antProject.getTargets().values()) {
            if (target.getName().isEmpty()) {
                continue;
            }

            final String startNodeName = target.getName();
            final AntTargetNode startNode =
                    (AntTargetNode) graph.getNode(startNodeName);
            if (!targetMap.containsValue(startNodeName)) {
                startNode.setAttributes(Node.ATTR_PREFIXED_NODE);
            }

            for (String dependency : Collections.list(target.getDependencies())) {
                createLink(graph, null, startNode, dependency);
            }

            taskLinkFinder.setGraph(graph);
            taskLinkFinder.setStartNode(startNode);
            for (final Task element : target.getTasks()) {
                taskLinkFinder.visit(element.getRuntimeConfigurableWrapper());
            }
        }

        return graph;
    }

    /**
     * Creates a new link. The end node will be created if needed with the
     * MISSING_NODE attribute set.
     *
     * @param graph
     *            owning graph
     * @param linkName
     *            name of the created link, can be <code>null</code>.
     * @param startNode
     *            start node of the link.
     * @param endNodeName
     *            name of the end node.
     * @return a new link.
     * @throws DuplicateElementException
     *             if there is already a node name <code>endNodeName</code> in
     *             the graph.
     */
    private AntLink createLink(final Graph graph, final String linkName, final Node startNode,
            final String endNodeName) throws DuplicateElementException {
        Node endNode = graph.getNode(endNodeName);

        if (endNode == null) {
            LOG.warn("Target " + startNode + " has dependency to non existent target "
                    + endNodeName + ", creating a dummy node");
            endNode = graph.createNode(endNodeName);
            endNode.setAttributes(Node.ATTR_MISSING_NODE);
        }

        LOG.debug("Creating link from " + startNode + " to " + endNodeName);
        return (AntLink) graph.createLink(linkName, startNode, endNode);
    }
}