/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package org.apache.tools.ant;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;

import org.apache.tools.ant.launch.Launcher;
import org.apache.tools.ant.taskdefs.Definer;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.taskdefs.Typedef;

/**
 * Component creation and configuration.
 *
 * The class is based around handing component
 * definitions in an AntTypeTable.
 *
 * The old task/type methods have been kept
 * for backward compatibly.
 * Project will just delegate its calls to this class.
 *
 * A very simple hook mechanism is provided that allows users to plug
 * in custom code. It is also possible to replace the default behavior
 * (for example in an app embedding Ant)
 *
 * @since Ant1.6
 */
public class ComponentHelper  {
    /** Map of component name to lists of restricted definitions */
    private final Map<String, List<AntTypeDefinition>> restrictedDefinitions = new HashMap<>();

    /** Map from component name to anttypedefinition */
    private final Hashtable<String, AntTypeDefinition> antTypeTable = new Hashtable<>();

    /** Map of tasks generated from antTypeTable */
    private final Hashtable<String, Class<?>> taskClassDefinitions = new Hashtable<>();

    /** flag to rebuild taskClassDefinitions */
    private boolean rebuildTaskClassDefinitions = true;

    /** Map of types generated from antTypeTable */
    private final Hashtable<String, Class<?>> typeClassDefinitions = new Hashtable<>();

    /** flag to rebuild typeClassDefinitions */
    private boolean rebuildTypeClassDefinitions = true;

    /** Set of namespaces that have been checked for antlibs */
    private final HashSet<String> checkedNamespaces = new HashSet<>();

    /**
     * Stack of antlib contexts used to resolve definitions while
     *   processing antlib
     */
    private Stack<String> antLibStack = new Stack<>();

    /** current antlib uri */
    private String antLibCurrentUri = null;

    /**
     * this does not appear to be used anywhere in the Ant codebase
     * even via its accessors
     */
    private ComponentHelper next;

    /**
     * Project that owns a component helper
     */
    private Project project;

    /**
     * Error string when the file taskdefs/defaults.properties cannot be found
     */
    private static final String ERROR_NO_TASK_LIST_LOAD = "Can't load default task list";

    /**
     * Error string when the typedefs/defaults.properties cannot be found
     */
    private static final String ERROR_NO_TYPE_LIST_LOAD = "Can't load default type list";

    /**
     * reference under which we register ourselves with a project -{@value}
     */
    public static final String COMPONENT_HELPER_REFERENCE = "ant.ComponentHelper";

    /**
     * string used to control build.syspath policy {@value}
     */
    private static final String BUILD_SYSCLASSPATH_ONLY = "only";

    /**
     * special name of ant's property task -{@value}. There is some
     * contrived work here to enable this early.
     */
    private static final String ANT_PROPERTY_TASK = "property";

    // {tasks, types}
    private static Properties[] defaultDefinitions = new Properties[2];

     /**
     * Get the project.
     * @return the project owner of this helper.
     */
     public Project getProject() {
         return project;
     }

    /**
     * Find a project component for a specific project, creating
     * it if it does not exist.
     * @param project the project.
     * @return the project component for a specific project.
     */
    public static ComponentHelper getComponentHelper(Project project) {
        if (project == null) {
            return null;
        }
        // Singleton for now, it may change (per/classloader)
        ComponentHelper ph = project.getReference(COMPONENT_HELPER_REFERENCE);
        if (ph != null) {
            return ph;
        }
        ph = new ComponentHelper();
        ph.setProject(project);

        project.addReference(COMPONENT_HELPER_REFERENCE, ph);
        return ph;
    }

    /**
     * Creates a new ComponentHelper instance.
     */
    protected ComponentHelper() {
    }

    /**
     * Set the next chained component helper.
     *
     * @param next the next chained component helper.
     */
    public void setNext(ComponentHelper next) {
        this.next = next;
    }

    /**
     * Get the next chained component helper.
     *
     * @return the next chained component helper.
     */
    public ComponentHelper getNext() {
        return next;
    }

    /**
     * Sets the project for this component helper.
     *
     * @param project the project for this helper.
     */
    public void setProject(Project project) {
        this.project = project;
    }

    /**
     * @return A copy of the CheckedNamespace.
     */
    private synchronized Set<String> getCheckedNamespace() {
        @SuppressWarnings("unchecked")
        final Set<String> result = (Set<String>) checkedNamespaces.clone();
        return result;
    }

    /**
     * @return A deep copy of the restrictedDefinition
     */
    private Map<String, List<AntTypeDefinition>> getRestrictedDefinition() {
        final Map<String, List<AntTypeDefinition>> result = new HashMap<>();
        synchronized (restrictedDefinitions) {
            for (Map.Entry<String, List<AntTypeDefinition>> entry : restrictedDefinitions.entrySet()) {
                List<AntTypeDefinition> entryVal = entry.getValue();
                synchronized (entryVal) {
                    //copy the entryVal
                    entryVal = new ArrayList<>(entryVal);
                }
                result.put(entry.getKey(), entryVal);
            }
        }
        return result;
    }


    /**
     * Used with creating child projects. Each child
     * project inherits the component definitions
     * from its parent.
     * @param helper the component helper of the parent project.
     */
    public void initSubProject(ComponentHelper helper) {
        // add the types of the parent project
        @SuppressWarnings("unchecked")
        final Hashtable<String, AntTypeDefinition> typeTable
                = (Hashtable<String, AntTypeDefinition>) helper.antTypeTable.clone();
        synchronized (antTypeTable) {
            for (AntTypeDefinition def : typeTable.values()) {
                antTypeTable.put(def.getName(), def);
            }
        }
        // add the parsed namespaces of the parent project
        Set<String> inheritedCheckedNamespace = helper.getCheckedNamespace();
        synchronized (this) {
            checkedNamespaces.addAll(inheritedCheckedNamespace);
        }
        Map<String, List<AntTypeDefinition>> inheritedRestrictedDef
                = helper.getRestrictedDefinition();
        synchronized (restrictedDefinitions) {
            restrictedDefinitions.putAll(inheritedRestrictedDef);
        }
    }

    /**
     * Factory method to create the components.
     *
     * This should be called by UnknownElement.
     *
     * @param ue The Unknown Element creating this component.
     * @param ns Namespace URI. Also available as ue.getNamespace().
     * @param componentType The component type,
     *                       Also available as ue.getComponentName().
     * @return the created component.
     * @throws BuildException if an error occurs.
     */
    public Object createComponent(UnknownElement ue, String ns, String componentType)
            throws BuildException {
        Object component = createComponent(componentType);
        if (component instanceof Task) {
            Task task = (Task) component;
            task.setLocation(ue.getLocation());
            task.setTaskType(componentType);
            task.setTaskName(ue.getTaskName());
            task.setOwningTarget(ue.getOwningTarget());
            task.init();
        }
        return component;
    }

    /**
     * Create an object for a component.
     *
     * @param componentName the name of the component, if
     *                      the component is in a namespace, the
     *                      name is prefixed with the namespace uri and ":".
     * @return the class if found or null if not.
     */
    public Object createComponent(String componentName) {
        AntTypeDefinition def = getDefinition(componentName);
        return def == null ? null : def.create(project);
    }

    /**
     * Return the class of the component name.
     *
     * @param componentName the name of the component, if
     *                      the component is in a namespace, the
     *                      name is prefixed with the namespace uri and ":".
     * @return the class if found or null if not.
     */
    public Class<?> getComponentClass(String componentName) {
        AntTypeDefinition def = getDefinition(componentName);
        return def == null ? null : def.getExposedClass(project);
    }

    /**
     * Return the antTypeDefinition for a componentName.
     * @param componentName the name of the component.
     * @return the ant definition or null if not present.
     */
    public AntTypeDefinition getDefinition(String componentName) {
        checkNamespace(componentName);
        return antTypeTable.get(componentName);
    }

    /**
     * This method is initialization code implementing the original ant component
     * loading from /org/apache/tools/ant/taskdefs/default.properties
     * and /org/apache/tools/ant/types/default.properties.
     */
    public void initDefaultDefinitions() {
        initTasks();
        initTypes();
        new DefaultDefinitions(this).execute();
    }

    /**
     * Adds a new task definition to the project.
     * Attempting to override an existing definition with an
     * equivalent one (i.e. with the same classname) results in
     * a verbose log message. Attempting to override an existing definition
     * with a different one results in a warning log message.
     *
     * @param taskName The name of the task to add.
     *                 Must not be <code>null</code>.
     * @param taskClass The full name of the class implementing the task.
     *                  Must not be <code>null</code>.
     *
     * @exception BuildException if the class is unsuitable for being an Ant
     *                           task. An error level message is logged before
     *                           this exception is thrown.
     *
     * @see #checkTaskClass(Class)
     */
    public void addTaskDefinition(String taskName, Class<?> taskClass) {
        checkTaskClass(taskClass);
        AntTypeDefinition def = new AntTypeDefinition();
        def.setName(taskName);
        def.setClassLoader(taskClass.getClassLoader());
        def.setClass(taskClass);
        def.setAdapterClass(TaskAdapter.class);
        def.setClassName(taskClass.getName());
        def.setAdaptToClass(Task.class);
        updateDataTypeDefinition(def);
    }

    /**
     * Checks whether or not a class is suitable for serving as Ant task.
     * Ant task implementation classes must be public, concrete, and have
     * a no-arg constructor.
     *
     * @param taskClass The class to be checked.
     *                  Must not be <code>null</code>.
     *
     * @exception BuildException if the class is unsuitable for being an Ant
     *                           task. An error level message is logged before
     *                           this exception is thrown.
     */
    public void checkTaskClass(final Class<?> taskClass) throws BuildException {
        if (!Modifier.isPublic(taskClass.getModifiers())) {
            final String message = taskClass + " is not public";
            project.log(message, Project.MSG_ERR);
            throw new BuildException(message);
        }
        if (Modifier.isAbstract(taskClass.getModifiers())) {
            final String message = taskClass + " is abstract";
            project.log(message, Project.MSG_ERR);
            throw new BuildException(message);
        }
        try {
            taskClass.getConstructor((Class[]) null);
            // don't have to check for public, since
            // getConstructor finds public constructors only.
        } catch (NoSuchMethodException e) {
            final String message = "No public no-arg constructor in " + taskClass;
            project.log(message, Project.MSG_ERR);
            throw new BuildException(message);
        }
        if (!Task.class.isAssignableFrom(taskClass)) {
            TaskAdapter.checkTaskClass(taskClass, project);
        }
    }

    /**
     * Returns the current task definition hashtable. The returned hashtable is
     * "live" and so should not be modified.  Also, the returned table may be
     * modified asynchronously.
     *
     * @return a map of from task name to implementing class
     *         (String to Class).
     */
    public Hashtable<String, Class<?>> getTaskDefinitions() {
        synchronized (taskClassDefinitions) {
            synchronized (antTypeTable) {
                if (rebuildTaskClassDefinitions) {
                    taskClassDefinitions.clear();
                    antTypeTable.entrySet().stream()
                            .filter(e -> e.getValue().getExposedClass(project) != null
                                    && Task.class.isAssignableFrom(e.getValue().getExposedClass(project)))
                            .forEach(e -> taskClassDefinitions.put(e.getKey(),
                                    e.getValue().getTypeClass(project)));
                    rebuildTaskClassDefinitions = false;
                }
            }
        }
        return taskClassDefinitions;
    }

    /**
     * Returns the current type definition hashtable. The returned hashtable is
     * "live" and so should not be modified.
     *
     * @return a map of from type name to implementing class
     *         (String to Class).
     */
    public Hashtable<String, Class<?>> getDataTypeDefinitions() {
        synchronized (typeClassDefinitions) {
            synchronized (antTypeTable) {
                if (rebuildTypeClassDefinitions) {
                    typeClassDefinitions.clear();
                    antTypeTable.entrySet().stream()
                            .filter(e -> e.getValue().getExposedClass(project) != null
                                    && !Task.class.isAssignableFrom(e.getValue().getExposedClass(project)))
                            .forEach(e -> typeClassDefinitions.put(e.getKey(),
                                    e.getValue().getTypeClass(project)));
                    rebuildTypeClassDefinitions = false;
                }
            }
        }
        return typeClassDefinitions;
    }

    /**
     * This returns a list of restricted definitions for a name.
     * The returned List is "live" and so should not be modified.
     * Also, the returned list may be modified asynchronously.
     * Any access must be guarded with a lock on the list itself.
     *
     * @param componentName the name to use.
     * @return the list of restricted definitions for a particular name.
     */
    public List<AntTypeDefinition> getRestrictedDefinitions(String componentName) {
        synchronized (restrictedDefinitions) {
            return restrictedDefinitions.get(componentName);
        }
    }

    /**
     * Adds a new datatype definition.
     * Attempting to override an existing definition with an
     * equivalent one (i.e. with the same classname) results in
     * a verbose log message. Attempting to override an existing definition
     * with a different one results in a warning log message, but the
     * definition is changed.
     *
     * @param typeName The name of the datatype.
     *                 Must not be <code>null</code>.
     * @param typeClass The full name of the class implementing the datatype.
     *                  Must not be <code>null</code>.
     */
    public void addDataTypeDefinition(String typeName, Class<?> typeClass) {
        final AntTypeDefinition def = new AntTypeDefinition();
        def.setName(typeName);
        def.setClass(typeClass);
        updateDataTypeDefinition(def);
        project.log(" +User datatype: " + typeName + "     " + typeClass.getName(),
                Project.MSG_DEBUG);
    }

    /**
     * Describe <code>addDataTypeDefinition</code> method here.
     *
     * @param def an <code>AntTypeDefinition</code> value.
     */
    public void addDataTypeDefinition(AntTypeDefinition def) {
        if (!def.isRestrict()) {
           updateDataTypeDefinition(def);
        } else {
            updateRestrictedDefinition(def);
        }
    }

    /**
     * Returns the current datatype definition hashtable. The returned
     * hashtable is "live" and so should not be modified.
     *
     * @return a map of from datatype name to datatype definition
     *         (String to {@link AntTypeDefinition}).
     */
    public Hashtable<String, AntTypeDefinition> getAntTypeTable() {
        return antTypeTable;
    }

    /**
     * Creates a new instance of a task.
     *
     *  Called from Project.createTask(), which can be called by tasks.
     *
     * @param taskType The name of the task to create an instance of.
     *                 Must not be <code>null</code>.
     *
     * @return an instance of the specified task, or <code>null</code> if
     *         the task name is not recognised.
     *
     * @exception BuildException if the task name is recognised but task
     *                           creation fails.
     */
    public Task createTask(String taskType) throws BuildException {
        Task task = createNewTask(taskType);
        if (task == null && taskType.equals(ANT_PROPERTY_TASK)) {
            // quick fix for Ant.java use of property before
            // initializing the project
            addTaskDefinition(ANT_PROPERTY_TASK, Property.class);
            task = createNewTask(taskType);
        }
        return task;
    }

    /**
     * Creates a new instance of a task.
     * @since ant1.6
     * @param taskType The name of the task to create an instance of.
     *                 Must not be <code>null</code>.
     *
     * @return an instance of the specified task, or <code>null</code> if
     *         the task name is not recognised.
     *
     * @exception BuildException if the task name is recognised but task
     *                           creation fails.
     */
    private Task createNewTask(String taskType) throws BuildException {
        Class<?> c = getComponentClass(taskType);
        if (c == null || !(Task.class.isAssignableFrom(c))) {
            return null;
        }
        Object obj = createComponent(taskType);
        if (obj == null) {
            return null;
        }
        if (!(obj instanceof Task)) {
            throw new BuildException("Expected a Task from '" + taskType
                    + "' but got an instance of " + obj.getClass().getName() + " instead");
        }
        Task task = (Task) obj;
        task.setTaskType(taskType);

        // set default value, can be changed by the user
        task.setTaskName(taskType);

        project.log("   +Task: " + taskType, Project.MSG_DEBUG);
        return task;
    }

    /**
     * Creates a new instance of a data type.
     *
     * @param typeName The name of the data type to create an instance of.
     *                 Must not be <code>null</code>.
     *
     * @return an instance of the specified data type, or <code>null</code> if
     *         the data type name is not recognised.
     *
     * @exception BuildException if the data type name is recognised but
     *                           instance creation fails.
     */
    public Object createDataType(String typeName) throws BuildException {
        return createComponent(typeName);
    }

    /**
     * Returns a description of the type of the given element.
     * <p>
     * This is useful for logging purposes.
     *
     * @param element The element to describe.
     *                Must not be <code>null</code>.
     *
     * @return a description of the element type.
     *
     * @since Ant 1.6
     */
    public String getElementName(Object element) {
        return getElementName(element, false);
    }

    /**
     * Returns a description of the type of the given element.
     * <p>
     * This is useful for logging purposes.
     *
     * @param o     The element to describe.
     *              Must not be <code>null</code>.
     * @param brief whether to use a brief description.
     * @return a description of the element type.
     *
     * @since Ant 1.7
     */
    public String getElementName(Object o, boolean brief) {
        //  PR: I do not know what to do if the object class
        //      has multiple defines
        //      but this is for logging only...
        Class<?> elementClass = o.getClass();
        String elementClassname = elementClass.getName();
        synchronized (antTypeTable) {
            for (AntTypeDefinition def : antTypeTable.values()) {
                if (elementClassname.equals(def.getClassName())
                        && (elementClass == def.getExposedClass(project))) {
                    String name = def.getName();
                    return brief ? name : "The <" + name + "> type";
                }
            }
        }
        return getUnmappedElementName(o.getClass(), brief);
    }

    /**
     * Convenient way to get some element name even when you may not have a
     * Project context.
     * @param p       The optional Project instance.
     * @param o       The element to describe.
     *                Must not be <code>null</code>.
     * @param brief   whether to use a brief description.
     * @return a description of the element type.
     * @since Ant 1.7
     */
    public static String getElementName(Project p, Object o, boolean brief) {
        if (p == null) {
            p = Project.getProject(o);
        }
        return p == null ? getUnmappedElementName(o.getClass(), brief) : getComponentHelper(p)
                .getElementName(o, brief);
    }

    private static String getUnmappedElementName(Class<?> c, boolean brief) {
        if (brief) {
            String name = c.getName();
            return name.substring(name.lastIndexOf('.') + 1);
        }
        return c.toString();
    }

    /**
     * Check if definition is a valid definition--it may be a
     * definition of an optional task that does not exist.
     * @param def the definition to test.
     * @return true if exposed type of definition is present.
     */
    private boolean validDefinition(AntTypeDefinition def) {
        return !(def.getTypeClass(project) == null || def.getExposedClass(project) == null);
    }

    /**
     * Check if two definitions are the same.
     * @param def  the new definition.
     * @param old the old definition.
     * @return true if the two definitions are the same.
     */
    private boolean sameDefinition(AntTypeDefinition def, AntTypeDefinition old) {
        boolean defValid = validDefinition(def);
        boolean sameValidity = (defValid == validDefinition(old));
        //must have same validity; then if they are valid they must also be the same:
        return sameValidity && (!defValid || def.sameDefinition(old, project));
    }

    /**
      * update the restricted definition table with a new or
      * modified definition.
      */
    private void updateRestrictedDefinition(AntTypeDefinition def) {
        String name = def.getName();
        List<AntTypeDefinition> list = null;
        synchronized (restrictedDefinitions) {
            list = restrictedDefinitions.computeIfAbsent(name, k -> new ArrayList<>());
        }
        // Check if the classname is already present and remove it
        // if it is
        synchronized (list) {
            for (Iterator<AntTypeDefinition> i = list.iterator(); i.hasNext();) {
                AntTypeDefinition current = i.next();
                if (current.getClassName().equals(def.getClassName())) {
                    i.remove();
                    break;
                }
            }
            list.add(def);
        }
    }

    /**
     * Update the component definition table with a new or
     * modified definition.
     * @param def the definition to update or insert.
     */
    private void updateDataTypeDefinition(AntTypeDefinition def) {
        String name = def.getName();
        synchronized (antTypeTable) {
            rebuildTaskClassDefinitions = true;
            rebuildTypeClassDefinitions = true;
            final AntTypeDefinition old = antTypeTable.get(name);
            if (old != null) {
                if (sameDefinition(def, old)) {
                    return;
                }
                Class<?> oldClass = old.getExposedClass(project);
                boolean isTask = oldClass != null && Task.class.isAssignableFrom(oldClass);
                project.log("Trying to override old definition of "
                        + (isTask ? "task " : "datatype ") + name, (def.similarDefinition(old,
                        project)) ? Project.MSG_VERBOSE : Project.MSG_WARN);
            }
            project.log(" +Datatype " + name + " " + def.getClassName(), Project.MSG_DEBUG);
            antTypeTable.put(name, def);
        }
    }

    /**
     * Called at the start of processing an antlib.
     * @param uri the uri that is associated with this antlib.
     */
    public void enterAntLib(String uri) {
        antLibCurrentUri = uri;
        antLibStack.push(uri);
    }

    /**
     * @return the current antlib uri.
     */
    public String getCurrentAntlibUri() {
        return antLibCurrentUri;
    }

    /**
     * Called at the end of processing an antlib.
     */
    public void exitAntLib() {
        antLibStack.pop();
        antLibCurrentUri = (antLibStack.isEmpty()) ? null : antLibStack.peek();
    }

    /**
     * Load ant's tasks.
     */
    private void initTasks() {
        ClassLoader classLoader = getClassLoader(null);
        Properties props = getDefaultDefinitions(false);
        for (String name : props.stringPropertyNames()) {
            AntTypeDefinition def = new AntTypeDefinition();
            def.setName(name);
            def.setClassName(props.getProperty(name));
            def.setClassLoader(classLoader);
            def.setAdaptToClass(Task.class);
            def.setAdapterClass(TaskAdapter.class);
            antTypeTable.put(name, def);
        }
    }

    private ClassLoader getClassLoader(ClassLoader classLoader) {
        String buildSysclasspath = project.getProperty(MagicNames.BUILD_SYSCLASSPATH);
        if (project.getCoreLoader() != null
            && !(BUILD_SYSCLASSPATH_ONLY.equals(buildSysclasspath))) {
            classLoader = project.getCoreLoader();
        }
        return classLoader;
    }

    /**
     * Load default task or type definitions - just the names,
     *  no class loading.
     * Caches results between calls to reduce overhead.
     * @param type true for typedefs, false for taskdefs
     * @return a mapping from definition names to class names
     * @throws BuildException if there was some problem loading
     *                        or parsing the definitions list
     */
    private static synchronized Properties getDefaultDefinitions(boolean type)
            throws BuildException {
        int idx = type ? 1 : 0;
        if (defaultDefinitions[idx] == null) {
            String resource = type ? MagicNames.TYPEDEFS_PROPERTIES_RESOURCE
                    : MagicNames.TASKDEF_PROPERTIES_RESOURCE;
            String errorString = type ? ERROR_NO_TYPE_LIST_LOAD : ERROR_NO_TASK_LIST_LOAD;
            try (InputStream in = ComponentHelper.class.getResourceAsStream(resource)) {
                if (in == null) {
                    throw new BuildException(errorString);
                }
                Properties p = new Properties();
                p.load(in);
                defaultDefinitions[idx] = p;
            } catch (IOException e) {
                throw new BuildException(errorString, e);
            }
        }
        return defaultDefinitions[idx];
    }

    /**
     * Load ant's datatypes.
     */
    private void initTypes() {
        ClassLoader classLoader = getClassLoader(null);
        Properties props = getDefaultDefinitions(true);
        for (String name : props.stringPropertyNames()) {
            AntTypeDefinition def = new AntTypeDefinition();
            def.setName(name);
            def.setClassName(props.getProperty(name));
            def.setClassLoader(classLoader);
            antTypeTable.put(name, def);
        }
    }

    /**
     * Called for each component name, check if the
     * associated URI has been examined for antlibs.
     * @param componentName the name of the component, which should include a URI
     *                      prefix if it is in a namespace
     */
    private synchronized void checkNamespace(String componentName) {
        String uri = ProjectHelper.extractUriFromComponentName(componentName);
        if (uri.isEmpty()) {
            uri = ProjectHelper.ANT_CORE_URI;
        }
        if (!uri.startsWith(ProjectHelper.ANTLIB_URI)) {
            return; // namespace that does not contain antlib
        }
        if (checkedNamespaces.contains(uri)) {
            return; // Already processed
        }
        checkedNamespaces.add(uri);

        if (antTypeTable.isEmpty()) {
            // Project instance doesn't know the tasks and types
            // defined in defaults.properties, likely created by the
            // user - without those definitions it cannot parse antlib
            // files as taskdef, typedef and friends are unknown
            initDefaultDefinitions();
        }
        Typedef definer = new Typedef();
        definer.setProject(project);
        definer.init();
        definer.setURI(uri);
        //there to stop error messages being "null"
        definer.setTaskName(uri);
        //if this is left out, bad things happen. like all build files break
        //on the first element encountered.
        definer.setResource(Definer.makeResourceFromURI(uri));
        // a fishing expedition :- ignore errors if antlib not present
        definer.setOnError(new Typedef.OnError(Typedef.OnError.POLICY_IGNORE));
        definer.execute();
    }

    /**
     * Handler called to do decent diagnosis on instantiation failure.
     * @param componentName component name.
     * @param type component type, used in error messages
     * @return a string containing as much diagnostics info as possible.
     */
    public String diagnoseCreationFailure(String componentName, String type) {
        StringWriter errorText = new StringWriter();
        PrintWriter out = new PrintWriter(errorText);
        out.println("Problem: failed to create " + type + " " + componentName);
        //class of problem
        boolean lowlevel = false;
        boolean jars = false;
        boolean definitions = false;
        boolean antTask;
        String home = System.getProperty(Launcher.USER_HOMEDIR);
        File libDir = new File(home, Launcher.USER_LIBDIR);
        String antHomeLib;
        boolean probablyIDE = false;
        String anthome = System.getProperty(MagicNames.ANT_HOME);
        if (anthome != null) {
            File antHomeLibDir = new File(anthome, "lib");
            antHomeLib = antHomeLibDir.getAbsolutePath();
        } else {
            //running under an IDE that doesn't set ANT_HOME
            probablyIDE = true;
            antHomeLib = "ANT_HOME" + File.separatorChar + "lib";
        }
        StringBuilder dirListingText = new StringBuilder();
        final String tab = "        -";
        dirListingText.append(tab);
        dirListingText.append(antHomeLib);
        dirListingText.append('\n');
        if (probablyIDE) {
            dirListingText.append(tab);
            dirListingText.append("the IDE Ant configuration dialogs");
        } else {
            dirListingText.append(tab);
            dirListingText.append(libDir);
            dirListingText.append('\n');
            dirListingText.append(tab);
            dirListingText.append("a directory added on the command line with the -lib argument");
        }
        String dirListing = dirListingText.toString();

        //look up the name
        AntTypeDefinition def = getDefinition(componentName);
        if (def == null) {
            //not a known type
            printUnknownDefinition(out, componentName, dirListing);
            definitions = true;
        } else {
            //we are defined, so it is an instantiation problem
            final String classname = def.getClassName();
            antTask = classname.startsWith("org.apache.tools.ant.");
            boolean optional = classname.startsWith("org.apache.tools.ant.taskdefs.optional");
            optional |= classname.startsWith("org.apache.tools.ant.types.optional");

            //start with instantiating the class.
            Class<?> clazz = null;
            try {
                clazz = def.innerGetTypeClass();
            } catch (ClassNotFoundException e) {
                jars = true;
                if (!optional) {
                    definitions = true;
                }
                printClassNotFound(out, classname, optional, dirListing);
            } catch (NoClassDefFoundError ncdfe) {
                jars = true;
                printNotLoadDependentClass(out, optional, ncdfe, dirListing);
            }
            //here we successfully loaded the class or failed.
            if (clazz != null) {
                //success: proceed with more steps
                try {
                    def.innerCreateAndSet(clazz, project);
                    //hey, there is nothing wrong with us
                    out.println("The component could be instantiated.");
                } catch (NoSuchMethodException e) {
                    lowlevel = true;
                    out.println("Cause: The class " + classname
                            + " has no compatible constructor.");

                } catch (InstantiationException e) {
                    lowlevel = true;
                    out.println("Cause: The class " + classname
                            + " is abstract and cannot be instantiated.");
                } catch (IllegalAccessException e) {
                    lowlevel = true;
                    out.println("Cause: The constructor for " + classname
                            + " is private and cannot be invoked.");
                } catch (InvocationTargetException ex) {
                    lowlevel = true;
                    Throwable t = ex.getTargetException();
                    out.println("Cause: The constructor threw the exception");
                    out.println(t.toString());
                    t.printStackTrace(out); //NOSONAR
                } catch (NoClassDefFoundError ncdfe) {
                    jars = true;
                    out.println("Cause:  A class needed by class " + classname
                            + " cannot be found: ");
                    out.println("       " + ncdfe.getMessage());
                    out.println("Action: Determine what extra JAR files are"
                            + " needed, and place them in:");
                    out.println(dirListing);
                }
            }
            out.println();
            out.println("Do not panic, this is a common problem.");
            if (definitions) {
                out.println("It may just be a typographical error in the build file "
                        + "or the task/type declaration.");
            }
            if (jars) {
                out.println("The commonest cause is a missing JAR.");
            }
            if (lowlevel) {
                out.println("This is quite a low level problem, which may need "
                        + "consultation with the author of the task.");
                if (antTask) {
                    out.println("This may be the Ant team. Please file a "
                            + "defect or contact the developer team.");
                } else {
                    out.println("This does not appear to be a task bundled with Ant.");
                    out.println("Please take it up with the supplier of the third-party " + type
                            + ".");
                    out.println("If you have written it yourself, you probably have a bug to fix.");
                }
            } else {
                out.println();
                out.println("This is not a bug; it is a configuration problem");
            }
        }
        out.flush();
        out.close();
        return errorText.toString();
    }

    /**
     * Print unknown definition.forking
     */
    private void printUnknownDefinition(PrintWriter out, String componentName, String dirListing) {
        boolean isAntlib = componentName.startsWith(MagicNames.ANTLIB_PREFIX);
        String uri = ProjectHelper.extractUriFromComponentName(componentName);
        out.println("Cause: The name is undefined.");
        out.println("Action: Check the spelling.");
        out.println("Action: Check that any custom tasks/types have been declared.");
        out.println("Action: Check that any <presetdef>/<macrodef>"
                + " declarations have taken place.");
        if (!uri.isEmpty()) {
            final List<AntTypeDefinition> matches = findTypeMatches(uri);
            if (matches.isEmpty()) {
                out.println("No types or tasks have been defined in this namespace yet");
                if (isAntlib) {
                    out.println();
                    out.println("This appears to be an antlib declaration. ");
                    out.println("Action: Check that the implementing library exists in one of:");
                    out.println(dirListing);
                }
            } else {
                out.println();
                out.println("The definitions in the namespace " + uri + " are:");
                for (AntTypeDefinition def : matches) {
                    String local = ProjectHelper.extractNameFromComponentName(def.getName());
                    out.println("    " + local);
                }
            }
        }
    }

    /**
     * Print class not found.
     */
    private void printClassNotFound(PrintWriter out, String classname, boolean optional,
            String dirListing) {
        out.println("Cause: the class " + classname + " was not found.");
        if (optional) {
            out.println("        This looks like one of Ant's optional components.");
            out.println("Action: Check that the appropriate optional JAR exists in");
            out.println(dirListing);
        } else {
            out.println("Action: Check that the component has been correctly declared");
            out.println("        and that the implementing JAR is in one of:");
            out.println(dirListing);
        }
    }

    /**
     * Print could not load dependent class.
     */
    private void printNotLoadDependentClass(PrintWriter out, boolean optional,
            NoClassDefFoundError ncdfe, String dirListing) {
        out.println("Cause: Could not load a dependent class "
                    +  ncdfe.getMessage());
        if (optional) {
            out.println("       It is not enough to have Ant's optional JARs");
            out.println("       you need the JAR files that the" + " optional tasks depend upon.");
            out.println("       Ant's optional task dependencies are" + " listed in the manual.");
        } else {
            out.println("       This class may be in a separate JAR" + " that is not installed.");
        }
        out.println("Action: Determine what extra JAR files are"
                + " needed, and place them in one of:");
        out.println(dirListing);
    }

    /**
     * Create a list of all definitions that match a prefix, usually the URI
     * of a library
     * @param prefix prefix to match off
     * @return the (possibly empty) list of definitions
     */
    private List<AntTypeDefinition> findTypeMatches(String prefix) {
        synchronized (antTypeTable) {
            return antTypeTable.values().stream().filter(def -> def.getName().startsWith(prefix))
                    .collect(Collectors.toList());
        }
    }
}
