GrandTask.java

// $Id$
/* ====================================================================
 * Copyright (c) 2002-2003, 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.tasks;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import net.ggtools.grand.ant.AntProject;
import net.ggtools.grand.exceptions.GrandException;
import net.ggtools.grand.filters.GraphFilter;
import net.ggtools.grand.graph.GraphProducer;
import net.ggtools.grand.graph.GraphWriter;
import net.ggtools.grand.log.AntLog;
import net.ggtools.grand.output.DotWriter;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.PropertySet;

/**
 * A task to create graphs.
 *
 * @author Christophe Labouisse
 */
public class GrandTask extends Task {

    /**
     * Field DEFAULT_OUTCONFIG_PREFIX.
     */
    private static final String DEFAULT_OUTCONFIG_PREFIX = "dot";

    /**
     * Field buildFile.
     */
    private File buildFile;

    /**
     * Field output.
     */
    private File output;

    /**
     * Field outputConfigurationFile.
     */
    private File outputConfigurationFile;

    /**
     * Field outputConfigurationPrefix.
     */
    private String outputConfigurationPrefix;

    /**
     * The sets of properties to pass to the graphed project.
     */
    private final List<PropertySet> propertySets = new ArrayList<PropertySet>();

    /**
     * Field filters.
     */
    private final List<FilterType> filters = new LinkedList<FilterType>();

    /**
     * Field showGraphName.
     */
    private boolean showGraphName = false;

    /**
     * Field inheritAll.
     */
    private boolean inheritAll = false;

    /**
     * Field properties.
     */
    private final List<Property> properties = new LinkedList<Property>();

    /**
     * Check the parameters validity before execution.
     *
     */
    private void checkParams() {
        if (output == null) {
            final String message = "required attribute missing";
            log(message, Project.MSG_ERR);
            throw new BuildException(message);
        }

        if (outputConfigurationFile != null && outputConfigurationPrefix != null) {
            final String message = "cannot specify both outputconfigfile and outputconfigprefix";
            log(message, Project.MSG_ERR);
            throw new BuildException(message);
        }

        for (FilterType filter : filters) {
            filter.checkParameters();
        }
    }

    /**
     * Method execute.
     * @see org.apache.tools.ant.Task#execute()
     */
    @Override
    public final void execute() {
        checkParams();

        final GraphProducer graphProject = initAntProject();

        log("Done loading project ", Project.MSG_VERBOSE);

        log("Setting up filter chain");
        GraphProducer producer = graphProject;
        int numFilters = 0;

        for (FilterType f : filters) {
            log("Adding filter " + f.getFilterName(), Project.MSG_VERBOSE);
            final GraphFilter filter = f.getFilter();
            filter.setProducer(producer);
            producer = filter;
            numFilters++;
        }

        if (numFilters > 0) {
            log("Loaded " + numFilters + " filter"
                    + ((numFilters > 1) ? "s" : ""));
        }

        try {
            Properties override = null;
            if (outputConfigurationFile != null) {
                log("Overriding default output configuration from " + outputConfigurationFile);
                override = new Properties();
                override.load(new FileInputStream(outputConfigurationFile));
            }
            if (outputConfigurationPrefix != null) {
                log("Overriding default output configuration from project properties "
                        + outputConfigurationPrefix + ".*");
                override = new Properties();
                for (String property : getProject().getProperties().keySet()) {
                    if (property.startsWith(outputConfigurationPrefix)) {
                        // one may use &quot; or &apos; or just ' in project properties
                        override.put(property.replace(outputConfigurationPrefix, DEFAULT_OUTCONFIG_PREFIX),
                                getProject().getProperty(property).replaceAll("'", "\""));
                    }
                }
            }

            final GraphWriter writer = new DotWriter(override);
            writer.setProducer(producer);
            writer.setShowGraphName(showGraphName);

            log("Writing output to " + output);
            writer.write(output);
        } catch (final IOException e) {
            log("Cannot write graph", Project.MSG_ERR);
            throw new BuildException("Cannot write graph", e);
        } catch (final GrandException e) {
            log("Cannot process graph", Project.MSG_ERR);
            throw new BuildException("Cannot write graph", e);
        }
    }

    /**
     * Create and initialize a GraphProducer according to the
     * task parameters.
     *
     * @return an initialized GraphProducer.
     */
    private GraphProducer initAntProject() {

        Project antProject;

        if (buildFile == null) {
            // Working with current project
            log("Using current project");
            antProject = getProject();
        } else {
            // Open a new project.
            log("Loading project " + buildFile);

            antProject = loadNewProject();
        }

        for (PropertySet ps : propertySets) {
            addAlmostAll(antProject, ps.getProperties());
        }

        if (properties.size() > 0) {
            for (Property prop : properties) {
                prop.setProject(antProject);
                prop.setTaskName("property");
                prop.execute();
            }
        }

        return new AntProject(antProject);
    }

    /**
     * Load an initialize a new project from a Ant build file.
     *
     * @return a new initialized Ant project.
     */
    private Project loadNewProject() {
        final Project antProject = new Project();

        // Set the current project listeners to the graphed project
        // in order to get some trace if we need to execute some tasks
        // in the graphed project.
        for (final BuildListener listener : getProject().getBuildListeners()) {
            antProject.addBuildListener(listener);
        }

        antProject.init();

        if (!inheritAll) {
            // set Java built-in properties separately,
            // b/c we won't inherit them.
            antProject.setSystemProperties();

        } else {
            // set all properties from calling project
            final Properties props = new Properties();
            for (Map.Entry<String, Object> entry : getProject().getProperties().entrySet()) {
                props.put(entry.getKey(), entry.getValue());
            }
            addAlmostAll(antProject, props);
        }

        antProject.setUserProperty("ant.file", buildFile.getAbsolutePath());
        final ProjectHelper loader = ProjectHelper.getProjectHelper();
        antProject.addReference("ant.projectHelper", loader);
        loader.parse(antProject, buildFile);

        getProject().copyUserProperties(antProject);

        return antProject;
    }

    /**
     * Method setProject.
     * @param project Project
     * @see org.apache.tools.ant.ProjectComponent#setProject(org.apache.tools.ant.Project)
     */
    @Override
    public final void setProject(final Project project) {
        super.setProject(project);
        AntLog.setCurrentProject(project);
        AntLog.setCurrentTask(this);
    }

    /**
     * Sets the buildFile.
     *
     * @param file File
     */
    public final void setBuildFile(final File file) {
        buildFile = file;
    }

    /**
     * Sets the output file.
     *
     * @param file File
     */
    public final void setOutput(final File file) {
        output = file;
    }

    /**
     * Set a property file to override the output default configuration.
     * @deprecated use {@link #setOutputConfigFile(File)}.
     * @param propertyFile File
     */
    @Deprecated
    public final void setPropertyFile(final File propertyFile) {
        log("Using of deprecated \"propertyfile\" attribute, use \"outputconfigfile\" from now on",
                Project.MSG_WARN);
        outputConfigurationFile = propertyFile;
    }

    /**
     * Set a property file to override the output default configuration.
     * @param propertyFile File
     */
    public final void setOutputConfigFile(final File propertyFile) {
        outputConfigurationFile = propertyFile;
    }

    /**
     * Set a property prefix to override the output default configuration.
     * @param propertyPrefix String
     */
    public final void setOutputConfigPrefix(final String propertyPrefix) {
        if (propertyPrefix == null) {
            return;
        }
        if (propertyPrefix.isEmpty()) {
            outputConfigurationPrefix = DEFAULT_OUTCONFIG_PREFIX;
            return;
        }
        outputConfigurationPrefix = propertyPrefix;
    }

    /**
     * Method setShowGraphName.
     * @param show boolean
     */
    public final void setShowGraphName(final boolean show) {
        showGraphName = show;
    }

    /**
     * If true, pass all properties to the new Ant project.
     * Defaults to true.
     * @param value if true pass all properties to the new Ant project.
     */
    public final void setInheritAll(final boolean value) {
        inheritAll = value;
    }

    /**
     * Add a filter to the task.
     * @param filter FilterType
     */
    public final void addFilter(final FilterType filter) {
        filters.add(filter);
    }

    /**
     * Add a new property to be passed to the graphed project.
     *
     * @param p the property to set.
     */
    public final void addProperty(final Property p) {
        properties.add(p);
    }

    /**
     * Set of properties to pass to the graphed project.
     *
     * @param ps property set to add
     */
    public final void addPropertyset(final PropertySet ps) {
        propertySets.add(ps);
    }

    /**
     * Copies all properties from the given table to the destination project -
     * omitting those that have already been set in the destination project as
     * well as properties named basedir or ant.file.
     * @param props properties to copy to the new project
     * @since Ant 1.6
     * @param destProject Project
     */
    private void addAlmostAll(final Project destProject, final Properties props) {
        for (Map.Entry<Object, Object> entry : props.entrySet()) {
            final String key = entry.getKey().toString();
            if ("basedir".equals(key) || "ant.file".equals(key)) {
                // basedir and ant.file should not be altered.
                continue;
            }

            final String value = entry.getValue().toString();
            // don't re-set user properties, avoid the warning message
            if (destProject.getProperty(key) == null) {
                // no user property
                destProject.setNewProperty(key, value);
            }
        }
    }

}