/*
 *  The Apache Software License, Version 1.1
 *
 *  Copyright (c) 2000-2002 The Apache Software Foundation.  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.
 *
 *  3. The end-user documentation included with the redistribution, if
 *  any, must include the following acknowlegement:
 *  "This product includes software developed by the
 *  Apache Software Foundation (http://www.apache.org/)."
 *  Alternately, this acknowlegement may appear in the software itself,
 *  if and wherever such third-party acknowlegements normally appear.
 *
 *  4. The names "The Jakarta Project", "Ant", and "Apache Software
 *  Foundation" must not be used to endorse or promote products derived
 *  from this software without prior written permission. For written
 *  permission, please contact apache@apache.org.
 *
 *  5. Products derived from this software may not be called "Apache"
 *  nor may "Apache" appear in their names without prior written
 *  permission of the Apache Group.
 *
 *  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 APACHE SOFTWARE FOUNDATION OR
 *  ITS 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.
 *  ====================================================================
 *
 *  This software consists of voluntary contributions made by many
 *  individuals on behalf of the Apache Software Foundation.  For more
 *  information on the Apache Software Foundation, please see
 *  <http://www.apache.org/>.
 */
package org.apache.tools.ant;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Stack;
import java.lang.reflect.Modifier;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import org.apache.tools.ant.types.DataTypeAdapterTask;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.FilterSet;
import org.apache.tools.ant.types.FilterSetCollection;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.taskdefs.Antlib;

/**
 *  Central representation of an Ant project. This class defines a Ant project
 *  with all of it's targets and tasks. It also provides the mechanism to kick
 *  off a build using a particular target name. <p>
 *
 *  This class also encapsulates methods which allow Files to be refered to
 *  using abstract path names which are translated to native system file paths
 *  at runtime as well as defining various project properties.
 *
 *@author     duncan@x180.com
 *@author     j_a_fernandez@yahoo.com
 *@created    February 27, 2002
 */

public class Project {

    /**
     *  Description of the Field
     */
    public final static int MSG_ERR = 0;
    /**
     *  Description of the Field
     */
    public final static int MSG_WARN = 1;
    /**
     *  Description of the Field
     */
    public final static int MSG_INFO = 2;
    /**
     *  Description of the Field
     */
    public final static int MSG_VERBOSE = 3;
    /**
     *  Description of the Field
     */
    public final static int MSG_DEBUG = 4;

    /**
     *  LoaderId for the CoreLoader.
     */
    public final static String CORELOADER_ID = null;

    /**
     *  Description of the Field
     */
    public final static String TASK_ROLE = "task";
    /**
     *  Description of the Field
     */
    public final static String DATATYPE_ROLE = "data-type";

    // private set of constants to represent the state
    // of a DFS of the Target dependencies
    private final static String VISITING = "VISITING";
    private final static String VISITED = "VISITED";

    private static String javaVersion;

    /**
     *  Description of the Field
     */
    public final static String JAVA_1_0 = "1.0";
    /**
     *  Description of the Field
     */
    public final static String JAVA_1_1 = "1.1";
    /**
     *  Description of the Field
     */
    public final static String JAVA_1_2 = "1.2";
    /**
     *  Description of the Field
     */
    public final static String JAVA_1_3 = "1.3";
    /**
     *  Description of the Field
     */
    public final static String JAVA_1_4 = "1.4";

    /**
     *  Description of the Field
     */
    public final static String TOKEN_START = FilterSet.DEFAULT_TOKEN_START;
    /**
     *  Description of the Field
     */
    public final static String TOKEN_END = FilterSet.DEFAULT_TOKEN_END;

    private final static String CORE_DEFINITIONS = "org/apache/tools/ant/antlib.xml";

    private String name;
    private String description;

    private Hashtable properties = new Hashtable();
    private Hashtable userProperties = new Hashtable();
    private Hashtable references = new Hashtable();
    private String defaultTarget;
    //    private Hashtable dataClassDefinitions = new Hashtable();
    //    private Hashtable taskClassDefinitions = new Hashtable();
    private Hashtable createdTasks = new Hashtable();
    private Hashtable targets = new Hashtable();
    private FilterSet globalFilterSet = new FilterSet();
    private FilterSetCollection globalFilters = new FilterSetCollection(globalFilterSet);
    private File baseDir;

    private Vector listeners = new Vector();

    /**
     *  The Ant core classloader - may be null if using system loader
     */
    private ClassLoader coreLoader = null;

    /**
     *  Records the latest task on a thread
     */
    private Hashtable threadTasks = new Hashtable();

    /**
     *  Store symbol tables
     */
    private SymbolTable symbols;

    static {

        // Determine the Java version by looking at available classes
        // java.lang.StrictMath was introduced in JDK 1.3
        // java.lang.ThreadLocal was introduced in JDK 1.2
        // java.lang.Void was introduced in JDK 1.1
        // Count up version until a NoClassDefFoundError ends the try

        try {
            javaVersion = JAVA_1_0;
            Class.forName("java.lang.Void");
            javaVersion = JAVA_1_1;
            Class.forName("java.lang.ThreadLocal");
            javaVersion = JAVA_1_2;
            Class.forName("java.lang.StrictMath");
            javaVersion = JAVA_1_3;
            Class.forName("java.lang.CharSequence");
            javaVersion = JAVA_1_4;
        } catch (ClassNotFoundException cnfe) {
            // swallow as we've hit the max class version that
            // we have
        }
    }

    private FileUtils fileUtils;


    /**
     *  <p>
     *
     *  Description: The only reason for this class is to make the
     *  LoadDefinition method visible in this package.</p>
     *
     *@author     jfernandez
     *@created    February 27, 2002
     */
    private class Corelib extends Antlib {
        /**
         *  Constructor for the Corelib object
         */
        Corelib() {
            super(Project.this);
            setLoaderid(CORELOADER_ID);
            getOnerror().setValue("ignore");
        }


        /**
         *  Description of the Method
         */
        public void loadCoreDefinitions() {
            getOnerror().setValue("report");
            super.loadDefinitions(CORE_DEFINITIONS);
        }
    }


    /**
     *  create a new ant project
     */
    public Project() {
        fileUtils = FileUtils.newFileUtils();
        symbols = new SymbolTable();
        symbols.setProject(this);
    }


    /**
     *  create a new ant project that inherits from caller project
     *
     *@param  p  the calling project
     */
    private Project(Project p) {
        fileUtils = FileUtils.newFileUtils();
        symbols = new SymbolTable(p.getSymbols());
        symbols.setProject(this);
        setCoreLoader(p.getCoreLoader());
    }


    /**
     *  Loads the core definitions into the Root project.
     */
    private void loadDefinitions() {
        // Initialize symbol table just in case
        symbols.addRole(TASK_ROLE, TaskContainer.class, TaskAdapter.class);
        symbols.addRole(DATATYPE_ROLE, TaskContainer.class,
                DataTypeAdapterTask.class);

        Corelib load = new Corelib();
        load.loadDefinitions();

        // If the most basic of tasks, "property", is not defined
        // then there was no antlib jars from where to load the descriptors
        // we should be doing bootstrapping, or ant.jar is not an antlib.
        if (!isDefinedOnRole(TASK_ROLE, "property")) {
            load.loadCoreDefinitions();

            if (!isDefinedOnRole(TASK_ROLE, "property")) {
                throw new BuildException("Can't load core definitions");
            }
        }
        autoLoadDefinitions();
    }

    private void autoLoadDefinitions() {
        DirectoryScanner ds = new DirectoryScanner();
        ds.setBasedir(new File(getProperty("ant.home"),"autolib"));
        ds.scan();
        String dirs[] = ds.getIncludedDirectories();
        for (int i = 0; i < dirs.length; i++) {
            autoLoad(ds.getBasedir(), dirs[i]);
        }
    }

    private void autoLoad(File base, String dir) {
        FileSet fs = new FileSet();
        fs.setProject(this);
        fs.setDir(new File(base, dir));
        fs.setIncludes("*.jar");

        Path cp = new Path(this);
        cp.addFileset(fs);
        if (cp.size() == 0) {
            return;
        }

        Antlib.FailureAction fa = new Antlib.FailureAction();
        fa.setValue("ignore");

        Antlib lib = new Antlib(this);
        lib.setClasspath(cp);
        lib.setLoaderid(dir);
        lib.setOnerror(fa);
        lib.loadDefinitions();
    }

    /**
     *  Creates a subproject of the current project.
     *
     *@return    Description of the Return Value
     */
    public Project createSubProject() {
        return new Project(this);
    }


    /**
     *  Initialise the project. This involves setting the default task
     *  definitions and loading the system properties.
     *
     *@exception  BuildException  Description of the Exception
     */
    public void init() throws BuildException {
        setJavaVersionProperty();
        setSystemProperties();
        if (!isRoleDefined(TASK_ROLE)) {
            // Top project, need to load the core definitions
            loadDefinitions();
        }
    }


    /**
     *  Sets the CoreLoader to the default of the Project object
     */
    private void setDefaultCoreLoader() {
        coreLoader = this.getClass().getClassLoader();
        if (coreLoader == null) {
            // This should only happen if ANT is being
            // loader by the Bootstrap classloader
            // This may be the case in JDK 1.1
            coreLoader = ClassLoader.getSystemClassLoader();
        }
    }


    /**
     *  Sets the coreLoader attribute of the Project object
     *
     *@param  coreLoader  The new coreLoader value
     */
    public void setCoreLoader(ClassLoader coreLoader) {
        if (coreLoader == null) {
            setDefaultCoreLoader();
        }
        else this.coreLoader = coreLoader;
    }


    /**
     *  Gets the coreLoader attribute of the Project object
     *
     *@return    The coreLoader value
     */
    public ClassLoader getCoreLoader() {
        if (coreLoader == null) {
            setDefaultCoreLoader();
        }
        return coreLoader;
    }


    /**
     *  Adds a feature to the BuildListener attribute of the Project object
     *
     *@param  listener  The feature to be added to the BuildListener attribute
     */
    public void addBuildListener(BuildListener listener) {
        listeners.addElement(listener);
    }


    /**
     *  Description of the Method
     *
     *@param  listener  Description of the Parameter
     */
    public void removeBuildListener(BuildListener listener) {
        listeners.removeElement(listener);
    }


    /**
     *  Gets the buildListeners attribute of the Project object
     *
     *@return    The buildListeners value
     */
    public Vector getBuildListeners() {
        return listeners;
    }


    /**
     *  Get the symbols associated with this project.
     *
     *@return    The symbols value
     */
    private SymbolTable getSymbols() {
        // Package protected on purpose
        return symbols;
    }


    /**
     *  Output a message to the log with the default log level of MSG_INFO
     *
     *@param  msg  text to log
     */

    public void log(String msg) {
        log(msg, MSG_INFO);
    }


    /**
     *  Output a message to the log with the given log level and an event scope
     *  of project
     *
     *@param  msg       text to log
     *@param  msgLevel  level to log at
     */
    public void log(String msg, int msgLevel) {
        fireMessageLogged(this, msg, msgLevel);
    }


    /**
     *  Output a message to the log with the given log level and an event scope
     *  of a task
     *
     *@param  task      task to use in the log
     *@param  msg       text to log
     *@param  msgLevel  level to log at
     */
    public void log(Task task, String msg, int msgLevel) {
        fireMessageLogged(task, msg, msgLevel);
    }


    /**
     *  Output a message to the log with the given log level and an event scope
     *  of a target
     *
     *@param  target    target to use in the log
     *@param  msg       text to log
     *@param  msgLevel  level to log at
     */
    public void log(Target target, String msg, int msgLevel) {
        fireMessageLogged(target, msg, msgLevel);
    }


    /**
     *  Gets the globalFilterSet attribute of the Project object
     *
     *@return    The globalFilterSet value
     */
    public FilterSet getGlobalFilterSet() {
        return globalFilterSet;
    }


    /**
     *  set a property. Any existing property of the same name is overwritten,
     *  unless it is a user property.
     *
     *@param  name   name of property
     *@param  value  new value of the property
     */
    public void setProperty(String name, String value) {
        // command line properties take precedence
        if (null != userProperties.get(name)) {
            log("Override ignored for user property " + name, MSG_VERBOSE);
            return;
        }

        if (null != properties.get(name)) {
            log("Overriding previous definition of property " + name,
                    MSG_VERBOSE);
        }

        log("Setting project property: " + name + " -> " +
                value, MSG_DEBUG);
        properties.put(name, value);
    }


    /**
     *  set a property. An existing property of the same name will not be
     *  overwritten.
     *
     *@param  name   name of property
     *@param  value  new value of the property
     *@since         1.5
     */
    public void setNewProperty(String name, String value) {
        if (null != properties.get(name)) {
            log("Override ignored for property " + name, MSG_VERBOSE);
            return;
        }
        log("Setting project property: " + name + " -> " +
                value, MSG_DEBUG);
        properties.put(name, value);
    }


    /**
     *  set a user property, which can not be overwritten by set/unset property
     *  calls
     *
     *@param  name   The new userProperty value
     *@param  value  The new userProperty value
     *@see           #setProperty(String,String)
     */
    public void setUserProperty(String name, String value) {
        log("Setting ro project property: " + name + " -> " +
                value, MSG_DEBUG);
        userProperties.put(name, value);
        properties.put(name, value);
    }


    /**
     *  Allows Project and subclasses to set a property unless its already
     *  defined as a user property. There are a few cases internally to Project
     *  that need to do this currently.
     *
     *@param  name   The new propertyInternal value
     *@param  value  The new propertyInternal value
     */
    private void setPropertyInternal(String name, String value) {
        if (null != userProperties.get(name)) {
            return;
        }
        properties.put(name, value);
    }


    /**
     *  query a property.
     *
     *@param  name  the name of the property
     *@return       the property value, or null for no match
     */
    public String getProperty(String name) {
        if (name == null) {
            return null;
        }
        String property = (String) properties.get(name);
        return property;
    }


    /**
     *  Replace ${} style constructions in the given value with the string value
     *  of the corresponding data types.
     *
     *@param  value               the string to be scanned for property
     *      references.
     *@return                     Description of the Return Value
     *@exception  BuildException  Description of the Exception
     */
    public String replaceProperties(String value)
             throws BuildException {
        return ProjectHelper.replaceProperties(this, value, properties);
    }


    /**
     *  query a user property.
     *
     *@param  name  the name of the property
     *@return       the property value, or null for no match
     */
    public String getUserProperty(String name) {
        if (name == null) {
            return null;
        }
        String property = (String) userProperties.get(name);
        return property;
    }


    /**
     *  get a copy of the property hashtable
     *
     *@return    the hashtable containing all properties, user included
     */
    public Hashtable getProperties() {
        Hashtable propertiesCopy = new Hashtable();

        Enumeration e = properties.keys();
        while (e.hasMoreElements()) {
            Object name = e.nextElement();
            Object value = properties.get(name);
            propertiesCopy.put(name, value);
        }

        return propertiesCopy;
    }


    /**
     *  get a copy of the user property hashtable
     *
     *@return    the hashtable user properties only
     */
    public Hashtable getUserProperties() {
        Hashtable propertiesCopy = new Hashtable();

        Enumeration e = userProperties.keys();
        while (e.hasMoreElements()) {
            Object name = e.nextElement();
            Object value = properties.get(name);
            propertiesCopy.put(name, value);
        }

        return propertiesCopy;
    }


    /**
     *  set the default target of the project
     *
     *@param  defaultTarget  The new defaultTarget value
     *@deprecated,           use setDefault
     *@see                   #setDefault(String)
     */
    public void setDefaultTarget(String defaultTarget) {
        this.defaultTarget = defaultTarget;
    }


    /**
     *  get the default target of the project
     *
     *@return    default target or null
     */
    public String getDefaultTarget() {
        return defaultTarget;
    }


    /**
     *  set the default target of the project XML attribute name.
     *
     *@param  defaultTarget  The new default value
     */
    public void setDefault(String defaultTarget) {
        this.defaultTarget = defaultTarget;
    }


    /**
     *  ant xml property. Set the project name as an attribute of this class,
     *  and of the property ant.project.name
     *
     *@param  name  The new name value
     */
    public void setName(String name) {
        setUserProperty("ant.project.name", name);
        this.name = name;
    }


    /**
     *  get the project name
     *
     *@return    name string
     */
    public String getName() {
        return name;
    }


    /**
     *  set the project description
     *
     *@param  description  text
     */
    public void setDescription(String description) {
        this.description = description;
    }


    /**
     *  get the project description
     *
     *@return    description or null if no description has been set
     */
    public String getDescription() {
        return description;
    }


    /**
     *@param  token  The feature to be added to the Filter attribute
     *@param  value  The feature to be added to the Filter attribute
     *@deprecated
     */
    public void addFilter(String token, String value) {
        if (token == null) {
            return;
        }

        globalFilterSet.addFilter(new FilterSet.Filter(token, value));
    }


    /**
     *@return        The filters value
     *@deprecated
     */
    public Hashtable getFilters() {
        // we need to build the hashtable dynamically
        return globalFilterSet.getFilterHash();
    }


    /**
     *  match basedir attribute in xml
     *
     *@param  baseD            project base directory.
     *@throws  BuildException  if the directory was invalid
     */
    public void setBasedir(String baseD) throws BuildException {
        setBaseDir(new File(baseD));
    }


    /**
     *  set the base directory; XML attribute. checks for the directory existing
     *  and being a directory type
     *
     *@param  baseDir          project base directory.
     *@throws  BuildException  if the directory was invalid
     */
    public void setBaseDir(File baseDir) throws BuildException {
        baseDir = fileUtils.normalize(baseDir.getAbsolutePath());
        if (!baseDir.exists()) {
            throw new BuildException("Basedir " + baseDir.getAbsolutePath() + " does not exist");
        }
        if (!baseDir.isDirectory()) {
            throw new BuildException("Basedir " + baseDir.getAbsolutePath() + " is not a directory");
        }
        this.baseDir = baseDir;
        setPropertyInternal("basedir", this.baseDir.getPath());
        String msg = "Project base dir set to: " + this.baseDir;
        log(msg, MSG_VERBOSE);
    }


    /**
     *  get the base directory of the project as a file object
     *
     *@return    the base directory. If this is null, then the base dir is not
     *      valid
     */
    public File getBaseDir() {
        if (baseDir == null) {
            try {
                setBasedir(".");
            } catch (BuildException ex) {
                ex.printStackTrace();
            }
        }
        return baseDir;
    }


    /**
     *  static query of the java version
     *
     *@return    something like "1.1" or "1.3"
     */
    public static String getJavaVersion() {
        return javaVersion;
    }


    /**
     *  set the ant.java.version property, also tests for unsupported JVM
     *  versions, prints the verbose log messages
     *
     *@throws  BuildException  if this Java version is not supported
     */
    public void setJavaVersionProperty() throws BuildException {
        setPropertyInternal("ant.java.version", javaVersion);

        // sanity check
        if (javaVersion == JAVA_1_0) {
            throw new BuildException("Ant cannot work on Java 1.0");
        }

        log("Detected Java version: " + javaVersion + " in: " + System.getProperty("java.home"), MSG_VERBOSE);

        log("Detected OS: " + System.getProperty("os.name"), MSG_VERBOSE);
    }


    /**
     *  turn all the system properties into ant properties. user properties
     *  still override these values
     */
    public void setSystemProperties() {
        Properties systemP = System.getProperties();
        Enumeration e = systemP.keys();
        while (e.hasMoreElements()) {
            Object name = e.nextElement();
            String value = systemP.get(name).toString();
            this.setPropertyInternal(name.toString(), value);
        }
    }


    /**
     *  Adds a feature to the ToLoader attribute of the Project object
     *
     *@param  loader  The feature to be added to the ToLoader attribute
     *@param  path    The feature to be added to the ToLoader attribute
     *@return         Description of the Return Value
     */
    public ClassLoader addToLoader(String loader, Path path) {
        if (loader == CORELOADER_ID) {
            // It is not possible to add more libraries to the CoreLoader
            // Just return it as is.
            return getCoreLoader();
        }
        return symbols.addToLoader(loader, path);
    }


    /**
     *  Adds a feature to the RoleDefinition attribute of the Project object
     *
     *@param  role       The feature to be added to the RoleDefinition attribute
     *@param  roleClass  The feature to be added to the RoleDefinition attribute
     *@param  adapter    The feature to be added to the RoleDefinition attribute
     *@return            Description of the Return Value
     */
    public boolean addRoleDefinition(String role,
            Class roleClass, Class adapter) {
        return symbols.addRole(role, roleClass, adapter);
    }


    /**
     *  test for a role name being in use already
     *
     *@param  name  the name to test
     *@return       true if it is a task or a datatype
     */
    public boolean isRoleDefined(String name) {
        return (symbols.getRole(name) != null);
    }


    /**
     *  Adds a feature to the DefinitionOnRole attribute of the Project object
     *
     *@param  role  The feature to be added to the DefinitionOnRole attribute
     *@param  type  The feature to be added to the DefinitionOnRole attribute
     *@param  clz   The feature to be added to the DefinitionOnRole attribute
     */
    public void addDefinitionOnRole(String role,
            String type, Class clz) {
        Class old = symbols.add(role, type, clz);
        // Special management for Tasks
        if (TASK_ROLE.equals(role) && null != old && !old.equals(clz)) {
            invalidateCreatedTasks(type);
        }
    }


    /**
     *  test for a name being in use already on this role
     *
     *@param  name  the name to test
     *@param  role  Description of the Parameter
     *@return       true if it is a task or a datatype
     */
    public boolean isDefinedOnRole(String role, String name) {
        return (symbols.get(role, name) != null);
    }


    /**
     *  add a new task definition, complain if there is an overwrite attempt
     *
     *@param  taskName         name of the task
     *@param  taskClass        full task classname
     *@throws  BuildException  and logs as Project.MSG_ERR for conditions, that
     *      will cause the task execution to fail.
     */
    public void addTaskDefinition(String taskName, Class taskClass)
             throws BuildException {
        addDefinitionOnRole(TASK_ROLE, taskName, taskClass);
    }


    /**
     *  Checks a class, whether it is suitable for serving as ant task.
     *
     *@param  taskClass        Description of the Parameter
     *@throws  BuildException  and logs as Project.MSG_ERR for conditions, that
     *      will cause the task execution to fail.
     *@deprecated              this is done now when added to SymbolTable
     */
    public void checkTaskClass(final Class taskClass) throws BuildException {
        if (!Task.class.isAssignableFrom(taskClass)) {
            TaskAdapter.checkTaskClass(taskClass, this);
        }
    }


    /**
     *  get the current task definition hashtable
     *
     *@return    The taskDefinitions value
     */
    public Hashtable getTaskDefinitions() {
        return symbols.getDefinitions(TASK_ROLE);
    }


    /**
     *  add a new datatype
     *
     *@param  typeName   name of the datatype
     *@param  typeClass  full datatype classname
     */
    public void addDataTypeDefinition(String typeName, Class typeClass) {
        addDefinitionOnRole(DATATYPE_ROLE, typeName, typeClass);
    }


    /**
     *  get the current task definition hashtable
     *
     *@return    The dataTypeDefinitions value
     */
    public Hashtable getDataTypeDefinitions() {
        return symbols.getDefinitions(DATATYPE_ROLE);
    }


    /**
     *  This call expects to add a <em>new</em> Target.
     *
     *@param  target              is the Target to be added to the current
     *      Project.
     *@see                        Project#addOrReplaceTarget to replace existing
     *      Targets.
     */
    public void addTarget(Target target) {
        String name = target.getName();
        if (targets.get(name) != null) {
            throw new BuildException("Duplicate target: `" + name + "'");
        }
        addOrReplaceTarget(name, target);
    }


    /**
     *  This call expects to add a <em>new</em> Target.
     *
     *@param  target              is the Target to be added to the current
     *      Project.
     *@param  targetName          is the name to use for the Target
     *@exception  BuildException  if the Target already exists in the project.
     *@see                        Project#addOrReplaceTarget to replace existing
     *      Targets.
     */
    public void addTarget(String targetName, Target target)
             throws BuildException {
        if (targets.get(targetName) != null) {
            throw new BuildException("Duplicate target: `" + targetName + "'");
        }
        addOrReplaceTarget(targetName, target);
    }


    /**
     *@param  target  is the Target to be added or replaced in the current
     *      Project.
     */
    public void addOrReplaceTarget(Target target) {
        addOrReplaceTarget(target.getName(), target);
    }


    /**
     *@param  target      is the Target to be added/replaced in the current
     *      Project.
     *@param  targetName  is the name to use for the Target
     */
    public void addOrReplaceTarget(String targetName, Target target) {
        String msg = " +Target: " + targetName;
        log(msg, MSG_DEBUG);
        target.setProject(this);
        targets.put(targetName, target);
    }


    /**
     *  get the target hashtable
     *
     *@return    hashtable, the contents of which can be cast to Target
     */
    public Hashtable getTargets() {
        return targets;
    }


    /**
     *  Create a new element instance on a Role
     *
     *@param  role  name of the role to use
     *@param  type  name of the element to create
     *@return       null if element unknown on this role
     */
    public Object createForRole(String role, String type) {
        SymbolTable.Factory f = symbols.get(role, type);
        if (f == null) {
            return null;
        }

        try {
            Object o = f.create(this);
            // Do special book keeping for ProjectComponents
            if (o instanceof ProjectComponent) {
                ((ProjectComponent) o).setProject(this);
                if (o instanceof Task) {
                    Task task = (Task) o;
                    task.setTaskType(type);

                    // set default value, can be changed by the user
                    task.setTaskName(type);
                    addCreatedTask(type, task);
                }
            }
            String msg = "   +" + role + ": " + type;
            log(msg, MSG_DEBUG);
            return o;
        } catch (Throwable t) {
            String msg = "Could not create " + role + " of type: "
                     + type + " due to " + t;
            throw new BuildException(msg, t);
        }
    }


    /**
     *@param  container  Description of the Parameter
     *@param  type       Description of the Parameter
     *@return            Description of the Return Value
     */
    public Object createInRole(Object container, String type) {
        Class clz = container.getClass();
        String roles[] = symbols.findRoles(clz);
        Object theOne = null;
        Method add = null;

        for (int i = 0; i < roles.length; i++) {
            Object o = createForRole(roles[i], type);
            if (o != null) {
                if (theOne != null) {
                    String msg = "Element " + type +
                            " is ambiguous for container " + clz.getName();
                    if (theOne instanceof RoleAdapter) {
                        theOne = ((RoleAdapter) theOne).getProxy();
                    }
                    if (o instanceof RoleAdapter) {
                        o = ((RoleAdapter) o).getProxy();
                    }

                    log(msg, MSG_ERR);
                    log("cannot distinguish between " +
                            theOne.getClass().getName() +
                            " and " + o.getClass().getName(), MSG_ERR);
                    throw new BuildException(msg);
                }
                theOne = o;
                add = symbols.getRole(roles[i]).getInterfaceMethod();
            }
        }
        if (theOne != null) {
            try {
                add.invoke(container, new Object[]{theOne});
            } catch (InvocationTargetException ite) {
                if (ite.getTargetException() instanceof BuildException) {
                    throw (BuildException) ite.getTargetException();
                }
                throw new BuildException(ite.getTargetException());
            } catch (Exception e) {
                throw new BuildException(e);
            }
        }
        return theOne;
    }


    /**
     *  create a new task instance
     *
     *@param  taskType         name of the task
     *@return                  null if the task name is unknown
     *@throws  BuildException  when task creation goes bad
     */
    public Task createTask(String taskType) throws BuildException {
        return (Task) createForRole(TASK_ROLE, taskType);
    }


    /**
     *  Keep a record of all tasks that have been created so that they can be
     *  invalidated if a taskdef overrides the definition.
     *
     *@param  type  The feature to be added to the CreatedTask attribute
     *@param  task  The feature to be added to the CreatedTask attribute
     */
    private void addCreatedTask(String type, Task task) {
        synchronized (createdTasks) {
            Vector v = (Vector) createdTasks.get(type);
            if (v == null) {
                v = new Vector();
                createdTasks.put(type, v);
            }
            v.addElement(task);
        }
    }


    /**
     *  Mark tasks as invalid which no longer are of the correct type for a
     *  given taskname.
     *
     *@param  type  Description of the Parameter
     */
    private void invalidateCreatedTasks(String type) {
        synchronized (createdTasks) {
            Vector v = (Vector) createdTasks.get(type);
            if (v != null) {
                Enumeration enum = v.elements();
                while (enum.hasMoreElements()) {
                    Task t = (Task) enum.nextElement();
                    t.markInvalid();
                }
                v.removeAllElements();
                createdTasks.remove(type);
            }
        }
    }


    /**
     *  create a new DataType instance
     *
     *@param  typeName         name of the datatype
     *@return                  null if the datatype name is unknown
     *@throws  BuildException  when datatype creation goes bad
     */
    public Object createDataType(String typeName) throws BuildException {
        // This is to make the function backward compatible
        // Since we know if it returning an adapter for it
        DataTypeAdapterTask dt =
                (DataTypeAdapterTask) createForRole(DATATYPE_ROLE, typeName);
        return (dt != null ? dt.getProxy() : null);
    }


    /**
     *  execute the sequence of targets, and the targets they depend on
     *
     *@param  targetNames      Description of the Parameter
     *@throws  BuildException  if the build failed
     */
    public void executeTargets(Vector targetNames) throws BuildException {
        Throwable error = null;

        for (int i = 0; i < targetNames.size(); i++) {
            executeTarget((String) targetNames.elementAt(i));
        }
    }


    /**
     *  Description of the Method
     *
     *@param  line     Description of the Parameter
     *@param  isError  Description of the Parameter
     */
    public void demuxOutput(String line, boolean isError) {
        Task task = (Task) threadTasks.get(Thread.currentThread());
        if (task == null) {
            fireMessageLogged(this, line, isError ? MSG_ERR : MSG_INFO);
        } else {
            if (isError) {
                task.handleErrorOutput(line);
            } else {
                task.handleOutput(line);
            }
        }
    }


    /**
     *  execute the targets and any targets it depends on
     *
     *@param  targetName       the target to execute
     *@throws  BuildException  if the build failed
     */
    public void executeTarget(String targetName) throws BuildException {

        // sanity check ourselves, if we've been asked to build nothing
        // then we should complain

        if (targetName == null) {
            String msg = "No target specified";
            throw new BuildException(msg);
        }

        // Sort the dependency tree, and run everything from the
        // beginning until we hit our targetName.
        // Sorting checks if all the targets (and dependencies)
        // exist, and if there is any cycle in the dependency
        // graph.
        Vector sortedTargets = topoSort(targetName, targets);

        int curidx = 0;
        Target curtarget;

        do {
            curtarget = (Target) sortedTargets.elementAt(curidx++);
            curtarget.performTasks();
        } while (!curtarget.getName().equals(targetName));
    }


    /**
     *  Return the canonical form of fileName as an absolute path. <p>
     *
     *  If fileName is a relative file name, resolve it relative to rootDir.</p>
     *
     *@param  fileName  Description of the Parameter
     *@param  rootDir   Description of the Parameter
     *@return           Description of the Return Value
     *@deprecated
     */
    public File resolveFile(String fileName, File rootDir) {
        return fileUtils.resolveFile(rootDir, fileName);
    }


    /**
     *  Description of the Method
     *
     *@param  fileName  Description of the Parameter
     *@return           Description of the Return Value
     */
    public File resolveFile(String fileName) {
        return fileUtils.resolveFile(baseDir, fileName);
    }


    /**
     *  Translate a path into its native (platform specific) format. <p>
     *
     *  This method uses the PathTokenizer class to separate the input path into
     *  its components. This handles DOS style paths in a relatively sensible
     *  way. The file separators are then converted to their platform specific
     *  versions.
     *
     *@param  to_process  the path to be converted
     *@return             the native version of to_process or an empty string if
     *      to_process is null or empty
     */
    public static String translatePath(String to_process) {
        if (to_process == null || to_process.length() == 0) {
            return "";
        }

        StringBuffer path = new StringBuffer(to_process.length() + 50);
        PathTokenizer tokenizer = new PathTokenizer(to_process);
        while (tokenizer.hasMoreTokens()) {
            String pathComponent = tokenizer.nextToken();
            pathComponent = pathComponent.replace('/', File.separatorChar);
            pathComponent = pathComponent.replace('\\', File.separatorChar);
            if (path.length() != 0) {
                path.append(File.pathSeparatorChar);
            }
            path.append(pathComponent);
        }

        return path.toString();
    }


    /**
     *  Convienence method to copy a file from a source to a destination. No
     *  filtering is performed.
     *
     *@param  sourceFile    Description of the Parameter
     *@param  destFile      Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(String sourceFile, String destFile) throws IOException {
        fileUtils.copyFile(sourceFile, destFile);
    }


    /**
     *  Convienence method to copy a file from a source to a destination
     *  specifying if token filtering must be used.
     *
     *@param  sourceFile    Description of the Parameter
     *@param  destFile      Description of the Parameter
     *@param  filtering     Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(String sourceFile, String destFile, boolean filtering)
             throws IOException {
        fileUtils.copyFile(sourceFile, destFile, filtering ? globalFilters : null);
    }


    /**
     *  Convienence method to copy a file from a source to a destination
     *  specifying if token filtering must be used and if source files may
     *  overwrite newer destination files.
     *
     *@param  sourceFile    Description of the Parameter
     *@param  destFile      Description of the Parameter
     *@param  filtering     Description of the Parameter
     *@param  overwrite     Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(String sourceFile, String destFile, boolean filtering,
            boolean overwrite) throws IOException {
        fileUtils.copyFile(sourceFile, destFile, filtering ? globalFilters : null, overwrite);
    }


    /**
     *  Convienence method to copy a file from a source to a destination
     *  specifying if token filtering must be used, if source files may
     *  overwrite newer destination files and the last modified time of <code>destFile</code>
     *  file should be made equal to the last modified time of <code>sourceFile</code>
     *  .
     *
     *@param  sourceFile            Description of the Parameter
     *@param  destFile              Description of the Parameter
     *@param  filtering             Description of the Parameter
     *@param  overwrite             Description of the Parameter
     *@param  preserveLastModified  Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(String sourceFile, String destFile, boolean filtering,
            boolean overwrite, boolean preserveLastModified)
             throws IOException {
        fileUtils.copyFile(sourceFile, destFile, filtering ? globalFilters : null,
                overwrite, preserveLastModified);
    }


    /**
     *  Convienence method to copy a file from a source to a destination. No
     *  filtering is performed.
     *
     *@param  sourceFile    Description of the Parameter
     *@param  destFile      Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(File sourceFile, File destFile) throws IOException {
        fileUtils.copyFile(sourceFile, destFile);
    }


    /**
     *  Convienence method to copy a file from a source to a destination
     *  specifying if token filtering must be used.
     *
     *@param  sourceFile    Description of the Parameter
     *@param  destFile      Description of the Parameter
     *@param  filtering     Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(File sourceFile, File destFile, boolean filtering)
             throws IOException {
        fileUtils.copyFile(sourceFile, destFile, filtering ? globalFilters : null);
    }


    /**
     *  Convienence method to copy a file from a source to a destination
     *  specifying if token filtering must be used and if source files may
     *  overwrite newer destination files.
     *
     *@param  sourceFile    Description of the Parameter
     *@param  destFile      Description of the Parameter
     *@param  filtering     Description of the Parameter
     *@param  overwrite     Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(File sourceFile, File destFile, boolean filtering,
            boolean overwrite) throws IOException {
        fileUtils.copyFile(sourceFile, destFile, filtering ? globalFilters : null, overwrite);
    }


    /**
     *  Convienence method to copy a file from a source to a destination
     *  specifying if token filtering must be used, if source files may
     *  overwrite newer destination files and the last modified time of <code>destFile</code>
     *  file should be made equal to the last modified time of <code>sourceFile</code>
     *  .
     *
     *@param  sourceFile            Description of the Parameter
     *@param  destFile              Description of the Parameter
     *@param  filtering             Description of the Parameter
     *@param  overwrite             Description of the Parameter
     *@param  preserveLastModified  Description of the Parameter
     *@throws  IOException
     *@deprecated
     */
    public void copyFile(File sourceFile, File destFile, boolean filtering,
            boolean overwrite, boolean preserveLastModified)
             throws IOException {
        fileUtils.copyFile(sourceFile, destFile, filtering ? globalFilters : null,
                overwrite, preserveLastModified);
    }


    /**
     *  Calls File.setLastModified(long time) in a Java 1.1 compatible way.
     *
     *@param  file                The new fileLastModified value
     *@param  time                The new fileLastModified value
     *@exception  BuildException  Description of the Exception
     *@deprecated
     */
    public void setFileLastModified(File file, long time) throws BuildException {
        if (getJavaVersion() == JAVA_1_1) {
            log("Cannot change the modification time of " + file
                     + " in JDK 1.1", Project.MSG_WARN);
            return;
        }
        fileUtils.setFileLastModified(file, time);
        log("Setting modification time for " + file, MSG_VERBOSE);
    }


    /**
     *  returns the boolean equivalent of a string, which is considered true if
     *  either "on", "true", or "yes" is found, ignoring case.
     *
     *@param  s  Description of the Parameter
     *@return    Description of the Return Value
     */
    public static boolean toBoolean(String s) {
        return (s.equalsIgnoreCase("on") ||
                s.equalsIgnoreCase("true") ||
                s.equalsIgnoreCase("yes"));
    }


    /**
     *  Topologically sort a set of Targets.
     *
     *@param  root                is the (String) name of the root Target. The
     *      sort is created in such a way that the sequence of Targets uptil the
     *      root target is the minimum possible such sequence.
     *@param  targets             is a Hashtable representing a "name to Target"
     *      mapping
     *@return                     a Vector of Strings with the names of the
     *      targets in sorted order.
     *@exception  BuildException  if there is a cyclic dependency among the
     *      Targets, or if a Target does not exist.
     */
    public final Vector topoSort(String root, Hashtable targets)
             throws BuildException {
        Vector ret = new Vector();
        Hashtable state = new Hashtable();
        Stack visiting = new Stack();

        // We first run a DFS based sort using the root as the starting node.
        // This creates the minimum sequence of Targets to the root node.
        // We then do a sort on any remaining unVISITED targets.
        // This is unnecessary for doing our build, but it catches
        // circular dependencies or missing Targets on the entire
        // dependency tree, not just on the Targets that depend on the
        // build Target.

        tsort(root, targets, state, visiting, ret);
        log("Build sequence for target `" + root + "' is " + ret, MSG_VERBOSE);
        for (Enumeration en = targets.keys(); en.hasMoreElements(); ) {
            String curTarget = (String) (en.nextElement());
            String st = (String) state.get(curTarget);
            if (st == null) {
                tsort(curTarget, targets, state, visiting, ret);
            } else if (st == VISITING) {
                throw new RuntimeException("Unexpected node in visiting state: " + curTarget);
            }
        }
        log("Complete build sequence is " + ret, MSG_VERBOSE);
        return ret;
    }


    // one step in a recursive DFS traversal of the Target dependency tree.
    // - The Hashtable "state" contains the state (VISITED or VISITING or null)
    // of all the target names.
    // - The Stack "visiting" contains a stack of target names that are
    // currently on the DFS stack. (NB: the target names in "visiting" are
    // exactly the target names in "state" that are in the VISITING state.)
    // 1. Set the current target to the VISITING state, and push it onto
    // the "visiting" stack.
    // 2. Throw a BuildException if any child of the current node is
    // in the VISITING state (implies there is a cycle.) It uses the
    // "visiting" Stack to construct the cycle.
    // 3. If any children have not been VISITED, tsort() the child.
    // 4. Add the current target to the Vector "ret" after the children
    //   have been visited. Move the current target to the VISITED state.
    //   "ret" now contains the sorted sequence of Targets upto the current
    //   Target.

    /**
     *  Description of the Method
     *
     *@param  root                Description of the Parameter
     *@param  targets             Description of the Parameter
     *@param  state               Description of the Parameter
     *@param  visiting            Description of the Parameter
     *@param  ret                 Description of the Parameter
     *@exception  BuildException  Description of the Exception
     */
    private final void tsort(String root, Hashtable targets,
            Hashtable state, Stack visiting,
            Vector ret)
             throws BuildException {
        state.put(root, VISITING);
        visiting.push(root);

        Target target = (Target) (targets.get(root));

        // Make sure we exist
        if (target == null) {
            StringBuffer sb = new StringBuffer("Target `");
            sb.append(root);
            sb.append("' does not exist in this project. ");
            visiting.pop();
            if (!visiting.empty()) {
                String parent = (String) visiting.peek();
                sb.append("It is used from target `");
                sb.append(parent);
                sb.append("'.");
            }

            throw new BuildException(new String(sb));
        }

        for (Enumeration en = target.getDependencies(); en.hasMoreElements(); ) {
            String cur = (String) en.nextElement();
            String m = (String) state.get(cur);
            if (m == null) {
                // Not been visited
                tsort(cur, targets, state, visiting, ret);
            } else if (m == VISITING) {
                // Currently visiting this node, so have a cycle
                throw makeCircularException(cur, visiting);
            }
        }

        String p = (String) visiting.pop();
        if (root != p) {
            throw new RuntimeException("Unexpected internal error: expected to pop " + root + " but got " + p);
        }
        state.put(root, VISITED);
        ret.addElement(target);
    }


    /**
     *  Description of the Method
     *
     *@param  end  Description of the Parameter
     *@param  stk  Description of the Parameter
     *@return      Description of the Return Value
     */
    private static BuildException makeCircularException(String end, Stack stk) {
        StringBuffer sb = new StringBuffer("Circular dependency: ");
        sb.append(end);
        String c;
        do {
            c = (String) stk.pop();
            sb.append(" <- ");
            sb.append(c);
        } while (!c.equals(end));
        return new BuildException(new String(sb));
    }


    /**
     *  Adds a feature to the Reference attribute of the Project object
     *
     *@param  name   The feature to be added to the Reference attribute
     *@param  value  The feature to be added to the Reference attribute
     */
    public void addReference(String name, Object value) {
        Object o = references.get(name);
        if (null != o && o != value
                 && (!(o instanceof RoleAdapter)
                 || ((RoleAdapter) o).getProxy() != value)) {
            log("Overriding previous definition of reference to " + name,
                    MSG_WARN);
        }
        log("Adding reference: " + name + " -> " + value, MSG_DEBUG);
        references.put(name, value);
    }


    /**
     *  Gets the references attribute of the Project object
     *
     *@return    The references value
     */
    public Hashtable getReferences() {
        return references;
    }


    /**
     *@param  key  Description of the Parameter
     *@return      The object with the "id" key.
     */
    public Object getReference(String key) {
        return references.get(key);
    }


    /**
     *  send build started event to the listeners
     */
    protected void fireBuildStarted() {
        BuildEvent event = new BuildEvent(this);
        for (int i = 0; i < listeners.size(); i++) {
            BuildListener listener = (BuildListener) listeners.elementAt(i);
            listener.buildStarted(event);
        }
    }


    /**
     *  send build finished event to the listeners
     *
     *@param  exception  exception which indicates failure if not null
     */
    protected void fireBuildFinished(Throwable exception) {
        BuildEvent event = new BuildEvent(this);
        event.setException(exception);
        for (int i = 0; i < listeners.size(); i++) {
            BuildListener listener = (BuildListener) listeners.elementAt(i);
            listener.buildFinished(event);
        }
    }


    /**
     *  send target started event to the listeners
     *
     *@param  target  Description of the Parameter
     */
    protected void fireTargetStarted(Target target) {
        BuildEvent event = new BuildEvent(target);
        for (int i = 0; i < listeners.size(); i++) {
            BuildListener listener = (BuildListener) listeners.elementAt(i);
            listener.targetStarted(event);
        }
    }


    /**
     *  send build finished event to the listeners
     *
     *@param  exception  exception which indicates failure if not null
     *@param  target     Description of the Parameter
     */
    protected void fireTargetFinished(Target target, Throwable exception) {
        BuildEvent event = new BuildEvent(target);
        event.setException(exception);
        for (int i = 0; i < listeners.size(); i++) {
            BuildListener listener = (BuildListener) listeners.elementAt(i);
            listener.targetFinished(event);
        }
    }


    /**
     *  Description of the Method
     *
     *@param  task  Description of the Parameter
     */
    protected void fireTaskStarted(Task task) {
        // register this as the current task on the current thread.
        threadTasks.put(Thread.currentThread(), task);
        BuildEvent event = new BuildEvent(task);
        for (int i = 0; i < listeners.size(); i++) {
            BuildListener listener = (BuildListener) listeners.elementAt(i);
            listener.taskStarted(event);
        }
    }


    /**
     *  Description of the Method
     *
     *@param  task       Description of the Parameter
     *@param  exception  Description of the Parameter
     */
    protected void fireTaskFinished(Task task, Throwable exception) {
        threadTasks.remove(Thread.currentThread());
        System.out.flush();
        System.err.flush();
        BuildEvent event = new BuildEvent(task);
        event.setException(exception);
        for (int i = 0; i < listeners.size(); i++) {
            BuildListener listener = (BuildListener) listeners.elementAt(i);
            listener.taskFinished(event);
        }
    }


    /**
     *  Description of the Method
     *
     *@param  event     Description of the Parameter
     *@param  message   Description of the Parameter
     *@param  priority  Description of the Parameter
     */
    private void fireMessageLoggedEvent(BuildEvent event, String message, int priority) {
        event.setMessage(message, priority);
        for (int i = 0; i < listeners.size(); i++) {
            BuildListener listener = (BuildListener) listeners.elementAt(i);
            listener.messageLogged(event);
        }
    }


    /**
     *  Description of the Method
     *
     *@param  project   Description of the Parameter
     *@param  message   Description of the Parameter
     *@param  priority  Description of the Parameter
     */
    protected void fireMessageLogged(Project project, String message, int priority) {
        BuildEvent event = new BuildEvent(project);
        fireMessageLoggedEvent(event, message, priority);
    }


    /**
     *  Description of the Method
     *
     *@param  target    Description of the Parameter
     *@param  message   Description of the Parameter
     *@param  priority  Description of the Parameter
     */
    protected void fireMessageLogged(Target target, String message, int priority) {
        BuildEvent event = new BuildEvent(target);
        fireMessageLoggedEvent(event, message, priority);
    }


    /**
     *  Description of the Method
     *
     *@param  task      Description of the Parameter
     *@param  message   Description of the Parameter
     *@param  priority  Description of the Parameter
     */
    protected void fireMessageLogged(Task task, String message, int priority) {
        BuildEvent event = new BuildEvent(task);
        fireMessageLoggedEvent(event, message, priority);
    }

}
