/*
 *  The Apache Software License, Version 1.1
 *
 *  Copyright (c) 1999 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.taskdefs;

import org.apache.tools.ant.*;
import org.apache.tools.ant.types.*;
import org.apache.tools.ant.taskdefs.*;
import org.xml.sax.*;
import javax.xml.parsers.*;

import java.util.*;
import java.util.zip.*;
import java.io.*;
import java.net.*;

/**
 * Make available the tasks and types from an Ant library. <pre>
 * &lt;antlib library="libname.jar" &gt;
 *   &lt;alias name="nameOnLib" as="newName" /&gt;
 * &lt;/antlib&gt;
 *
 * &lt;antlib file="libname.jar" override="true" /&gt;
 * </pre>
 *
 * @author minor changes by steve loughran, steve_l@iseran.com
 * @author <a href="j_a_fernandez@yahoo.com">Jose Alberto Fernandez</a>
 * @since ant1.5
 */
public class Antlib extends Task {

    /**
     * Location of descriptor in library
     */
    public static final String ANT_DESCRIPTOR = "META-INF/antlib.xml";

    /**
     * The named classloader to use.
     * Defaults to the default classLoader.
     */
    private String loaderId = "";

    /**
     * file attribute
     */
    private File file = null;
    /**
     * override attribute
     */
    private boolean override = false;
    /**
     * attribute to control failure when loading
     */
    private FailureAction onerror = new FailureAction();

    /**
     * classpath to build up
     */
    private Path classpath = null;

    /**
     * the manufacture set of classes to load
     */
    private Path loaderPath = null;

    /**
     * our little xml parse
     */
    private SAXParserFactory saxFactory;

    /**
     * table of aliases
     */
    private Vector aliases = new Vector();

    /**
     * Some internal constants.
     */
    private static final int FAIL = 0, REPORT = 1, IGNORE = 2;

    /**
     * Posible actions when classes are not found
     */
    public static class FailureAction extends EnumeratedAttribute {
        public String[] getValues() {
            return new String[]{"fail", "report", "ignore"};
        }
    }

    private static class DescriptorEnumeration implements Enumeration {

        /**
         * The name of the resource being searched for.
         */
        private String resourceName;

        /**
         * The index of the next file to search.
         */
        private int index;

        /**
         * The list of files to search
         */
        private File files[];

        /**
         * The URL of the next resource to return in the enumeration. If this
         * field is <code>null</code> then the enumeration has been completed,
         * i.e., there are no more elements to return.
         */
        private URL nextDescriptor;

        /**
         * Construct a new enumeration of resources of the given name found
         * within this class loader's classpath.
         *
         * @param name the name of the resource to search for.
         */
        DescriptorEnumeration(String fileNames[], String name) {
            this.resourceName = name;
            this.index = 0;
            this.files = new File[fileNames.length];
            for (int i = 0; i < files.length; i++) {
                files[i] = new File(fileNames[i]);
            }
            findNextDescriptor();
        }

        /**
         * Indicates whether there are more elements in the enumeration to
         * return.
         *
         * @return <code>true</code> if there are more elements in the
         *         enumeration; <code>false</code> otherwise.
         */
        public boolean hasMoreElements() {
            return (this.nextDescriptor != null);
        }

        /**
         * Returns the next resource in the enumeration.
         *
         * @return the next resource in the enumeration.
         */
        public Object nextElement() {
            URL ret = this.nextDescriptor;
            findNextDescriptor();
            return ret;
        }

        /**
         * Locates the next descriptor of the correct name in the files and
         * sets <code>nextDescriptor</code> to the URL of that resource. If no
         * more resources can be found, <code>nextDescriptor</code> is set to
         * <code>null</code>.
         */
        private void findNextDescriptor() {
            URL url = null;
            while (index < files.length && url == null) {
                try {
                    url = getDescriptorURL(files[index], this.resourceName);
                    index++;
                }
                catch (BuildException e) {
                    // ignore path elements which are not valid relative to the
                    // project
                }
            }
            this.nextDescriptor = url;
        }

        /**
         * Get an URL to a given resource in the given file which may
         * either be a directory or a zip file.
         *
         * @param file the file (directory or jar) in which to search for
         *             the resource. Must not be <code>null</code>.
         * @param resourceName the name of the resource for which a URL
         *                     is required. Must not be <code>null</code>.
         *
         * @return a URL to the required resource or <code>null</code> if the
         *         resource cannot be found in the given file object
         * @todo This code is extracted from AntClassLoader.getResourceURL
         *       I hate when that happens but the code there is too tied to
         *       the ClassLoader internals. Maybe we can find a nice place
         *       to put it where both can use it.
         */
        private URL getDescriptorURL(File file, String resourceName) {
            try {
                if (!file.exists()) {
                    return null;
                }

                if (file.isDirectory()) {
                    File resource = new File(file, resourceName);

                    if (resource.exists()) {
                        try {
                            return new URL("file:"+resource.toString());
                        } catch (MalformedURLException ex) {
                            return null;
                        }
                    }
                }
                else {
                    ZipFile zipFile = new ZipFile(file);
                    try {
                        ZipEntry entry = zipFile.getEntry(resourceName);
                        if (entry != null) {
                            try {
                                return new URL("jar:file:"+file.toString()+"!/"+entry);
                            } catch (MalformedURLException ex) {
                                return null;
                            }
                        }
                    }
                    finally {
                        zipFile.close();
                    }
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }

            return null;
        }

    }

    /**
     * constructor creates a validating sax parser
     */
    public Antlib() {
        super();
        // Default error action
        onerror.setValue("report");
        saxFactory = SAXParserFactory.newInstance();
        saxFactory.setValidating(false);
    }


    /**
     * constructor binds to a project and sets ignore mode on errors
     *
     * @param p Description of Parameter
     */
    public Antlib(Project p) {
        this();
        setProject(p);
    }


    /**
     * Set name of library to load. The library is located in $ANT_HOME/antlib.
     *
     * @param lib the name of library relative to $ANT_HOME/antlib.
     */
    public void setLibrary(String lib) {
        setFile(libraryFile("antlib", lib));
    }


    /**
     * Set file location of library to load.
     *
     * @param file the jar file for the library.
     */
    public void setFile(File file) {
        this.file = file;
    }

    /**
     * Set the ID of the ClassLoader to use for this library.
     *
     * @param id the id for the ClassLoader to use,
     *           <code>null</code> means use ANT's core classloader.
     */
    public void setLoaderid(String id) {
        this.loaderId = id;
    }

    /**
     * Set whether to override any existing definitions.
     *
     * @param override if true new definitions will replace existing ones.
     */
    public void setOverride(boolean override) {
        this.override = override;
    }


    /**
     * Get what to do if a definition cannot be loaded
     * This method is mostly used by the core when loading core tasks.
     *
     * @return what to do if a definition cannot be loaded
     */
    final protected FailureAction getOnerror() {
        return this.onerror;
    }


    /**
     * Set whether to fail if a definition cannot be loaded
     * Default is <code>true</code>.
     * This property is mostly used by the core when loading core tasks.
     *
     * @param failedonerror if true loading will stop if classes
     *                      cannot be instantiated
     */
    public void setOnerror(FailureAction onerror) {
        this.onerror = onerror;
    }


    /**
     * Create new Alias element.
     *
     * @return Description of the Returned Value
     */
    public Alias createAlias() {
        Alias als = new Alias();
        aliases.add(als);
        return als;
    }


    /**
     * Set the classpath to be used for this compilation
     *
     * @param cp The new Classpath value
     */
    public void setClasspath(Path cp) {
        if (classpath == null) {
            classpath = cp;
        }
        else {
            classpath.append(cp);
        }
    }


    /**
     * create a nested classpath element.
     *
     * @return  classpath to use
     */
    public Path createClasspath() {
        if (classpath == null) {
            classpath = new Path(project);
        }
        return classpath.createPath();
    }


    /**
     * Adds a reference to a CLASSPATH defined elsewhere
     *
     * @param r The new ClasspathRef value
     */
    public void setClasspathRef(Reference r) {
        createClasspath().setRefid(r);
    }


    /**
     * Obtain library file from ANT_HOME directory.
     *
     * @param lib the library name.
     * @return the File instance of the library
     */
    private File libraryFile(String homeSubDir, String lib) {
        // For the time being libraries live in $ANT_HOME/antlib.
        // The idea being that not to load all the jars there anymore
        String home = project.getProperty("ant.home");

        if (home == null) {
            throw new BuildException("ANT_HOME not set as required.");
        }

        return new File(new File(home, homeSubDir), lib);
    }

    /**
     * actually do the work of loading the library
     *
     * @exception BuildException Description of Exception
     */
    public void execute()
        throws BuildException {
        if (file == null && classpath == null) {
            String msg =
                "Must specify either library or file attribute or classpath.";
            throw new BuildException(msg, location);
        }
        if (file != null && !file.exists()) {
            String msg = "Cannot find library: " + file;
            throw new BuildException(msg, location);
        }

        loadDefinitions();
    }


    /**
     * Load definitions in library and classpath
     *
     * @exception BuildException failure to access the resource
     */
    public boolean loadDefinitions() throws BuildException {
        return loadDefinitions(ANT_DESCRIPTOR);
    }

    /**
     * Load definitions from resource name in library and classpath
     *
     * @param res the name of the resources to load
     * @exception BuildException failure to access the resource
     */
    final protected boolean loadDefinitions(String res)
        throws BuildException {
        Path path = makeLoaderClasspath();
        ClassLoader cl = makeClassLoader(path);
        boolean found = false;
        try {
            for (Enumeration e = getDescriptors(path, res); e.hasMoreElements(); ) {
                URL resURL = (URL)e.nextElement();
                InputStream is = resURL.openStream();
                loadDefinitions(cl, is);
                found = true;
            }
            if (!found && onerror.getIndex() != IGNORE) {
                String sPath = path.toString();
                if ("".equals(sPath.trim())) {
                    sPath = System.getProperty("java.classpath");
                }
                String msg = "Cannot find any " + res +
                    " antlib descriptors in: " + sPath;
                switch (onerror.getIndex()) {
                case FAIL:
                    throw new BuildException(msg);
                case REPORT:
                    log(msg, project.MSG_WARN);
                }
            }
        }
        catch (IOException io) {
            String msg = "Cannot load definitions from: " + res;
            switch (onerror.getIndex()) {
            case FAIL:
                throw new BuildException(msg, io);
            case REPORT:
                log(io.getMessage(), project.MSG_WARN);
            }
        }
        return found;
    }


    /**
     * Load definitions directly from InputStream.
     *
     * @param is InputStream for the Antlib descriptor.
     * @exception BuildException trouble
     */
    private void loadDefinitions(ClassLoader cl, InputStream is)
        throws BuildException {
        evaluateDescriptor(cl, processAliases(), is);
    }


    /**
     * get an Enumeration of URLs for all resouces corresponding to the
     * descriptor name.
     *
     * @param res the name of the resource to collect
     * @return input stream to the Descriptor or null if none existent
     * @exception BuildException io trouble, or it isnt a zipfile
     */
    private Enumeration getDescriptors(Path path, final String res)
        throws BuildException, IOException {
        if (loaderId == null) {
            // Path cannot be added to the CoreLoader so simply
            // ask for all instances of the resource descriptors
            return project.getCoreLoader().getResources(res);
        }

        return new DescriptorEnumeration(path.list(), res);
    }


    /**
     * turn the alias list to a property hashtable
     *
     * @return generated property hashtable
     */
    private Properties processAliases() {
        Properties p = new Properties();

        for (Enumeration e = aliases.elements(); e.hasMoreElements(); ) {
            Alias a = (Alias) e.nextElement();
            p.put(a.name, a.as);
        }
        return p;
    }


    /**
     * create the classpath for this library from the file passed in and
     * any classpath parameters
     *
     * @param file library file to use
     * @return classloader using te
     * @exception BuildException trouble creating the classloader
     */
    protected ClassLoader makeClassLoader(Path clspath)
        throws BuildException {
        if (loaderId == null) {
            log("Loading definitions from CORE, <classpath> ignored",
                project.MSG_VERBOSE);
            return project.getCoreLoader();
        }

        log("Using ClassLoader '" + loaderId + "' to load path: " + clspath,
            project.MSG_VERBOSE);
        return project.addToLoader(loaderId, clspath);
    }


    /**
     * Constructs the Path to add to the ClassLoader
     */
    private Path makeLoaderClasspath()
    {
        Path clspath = new Path(project);
        if (file != null) clspath.setLocation(file);
        //append any build supplied classpath
        if (classpath != null) {
            clspath.append(classpath);
        }
        return clspath;
    }

    /**
     * parse the antlib descriptor
     *
     * @param cl optional classloader
     * @param als alias list as property hashtable
     * @param is input stream to descriptor
     * @exception BuildException trouble
     */
    protected void evaluateDescriptor(ClassLoader cl,
                                      Properties als, InputStream is)
        throws BuildException {
        try {
            SAXParser saxParser = saxFactory.newSAXParser();
            Parser parser = saxParser.getParser();

            InputSource inputSource = new InputSource(is);
            //inputSource.setSystemId(uri); //URI is nasty for jar entries
            project.log("parsing descriptor for library: " + file,
                        Project.MSG_VERBOSE);
            saxParser.parse(inputSource, new AntLibraryHandler(cl, als));
        }
        catch (ParserConfigurationException exc) {
            throw new BuildException("Parser has not been configured correctly", exc);
        }
        catch (SAXParseException exc) {
            Location location =
                    new Location(ANT_DESCRIPTOR,
                    exc.getLineNumber(), exc.getColumnNumber());

            Throwable t = exc.getException();
            if (t instanceof BuildException) {
                BuildException be = (BuildException) t;
                if (be.getLocation() == Location.UNKNOWN_LOCATION) {
                    be.setLocation(location);
                }
                throw be;
            }
            throw new BuildException(exc.getMessage(), t, location);
        }
        catch (SAXException exc) {
            Throwable t = exc.getException();
            if (t instanceof BuildException) {
                throw (BuildException) t;
            }
            throw new BuildException(exc.getMessage(), t);
        }
        catch (IOException exc) {
            throw new BuildException("Error reading library descriptor", exc);
        }
        finally {
            if (is != null) {
                try {
                    is.close();
                }
                catch (IOException ioe) {
                    // ignore this
                }
            }
        }
    }


    /**
     * Parses the document describing the content of the
     * library. An inner class for access to Project.log
     */
    private class AntLibraryHandler extends HandlerBase {

        /**
         * our classloader
         */
        private final ClassLoader classloader;
        /**
         * the aliases
         */
        private final Properties aliasMap;
        /**
         * doc locator
         */
        private Locator locator = null;

        private int level = 0;

        private String name = null;
        private String className = null;
        private String adapter = null;

        /**
         * Constructor for the AntLibraryHandler object
         *
         * @param cl optional classloader
         * @param als alias list
         */
        AntLibraryHandler(ClassLoader classloader, Properties als) {
            this.classloader = classloader;
            this.aliasMap = als;
        }

        /**
         * Sets the DocumentLocator attribute of the AntLibraryHandler
         * object
         *
         * @param locator The new DocumentLocator value
         */
        public void setDocumentLocator(Locator locator) {
            this.locator = locator;
        }

        private void parseAttributes(String tag, AttributeList attrs)
            throws SAXParseException {
            name = null;
            className = null;
            adapter = null;

            for (int i = 0, last = attrs.getLength(); i < last; i++) {
                String key = attrs.getName(i);
                String value = attrs.getValue(i);

                if (key.equals("name")) {
                    name = value;
                }
                else if (key.equals("class")) {
                    className = value;
                }
                else if ("role".equals(tag) && key.equals("adapter")) {
                    adapter = value;
                }
                else {
                    throw new SAXParseException("Unexpected attribute \""
                                                + key + "\"", locator);
                }
            }
            if (name == null || className == null) {
                String msg = "Underspecified " + tag + " declaration.";
                throw new SAXParseException(msg, locator);
            }
        }

        /**
         * SAX callback handler
         *
         * @param tag XML tag
         * @param attrs attributes
         * @exception SAXParseException parse trouble
         */
        public void startElement(String tag, AttributeList attrs)
            throws SAXParseException {
            level ++;
            if ("antlib".equals(tag)) {
                if (level > 1) {
                    throw new SAXParseException("Unexpected element: " + tag,
                                                locator);
                }
                // No attributes to worry about
                return;
            }
            if (level == 1) {
                throw new SAXParseException("Missing antlib root element",
                                            locator);
            }

            // Must have the two attributes declared
            parseAttributes(tag, attrs);

            try {
                if ("role".equals(tag)) {
                    if (project.isRoleDefined(name)) {
                        String msg = "Cannot override role: " + name;
                        log(msg, Project.MSG_WARN);
                        return;
                    }
                    // Defining a new role
                    Class clz = loadClass(className);
                    if (clz != null) {
                        project.addRoleDefinition(name, clz,
                                                  (adapter == null? null :
                                                   loadClass(adapter)));
                    }
                    return;
                }

                // Defining a new element kind
                //check for name alias
                String alias = aliasMap.getProperty(name);
                if (alias != null) {
                    name = alias;
                }
                //catch an attempted override of an existing name
                if (!override && project.isDefinedOnRole(tag, name)) {
                    String msg = "Cannot override " + tag + ": " + name;
                    log(msg, Project.MSG_WARN);
                    return;
                }
                Class clz = loadClass(className);
                if (clz != null)
                    project.addDefinitionOnRole(tag, name, clz);
            }
            catch(BuildException be) {
                switch (onerror.getIndex()) {
                case FAIL:
                    throw new SAXParseException(be.getMessage(), locator, be);
                case REPORT:
                    project.log(be.getMessage(), project.MSG_WARN);
                    break;
                default:
                    project.log(be.getMessage(), project.MSG_DEBUG);
                }
            }
        }

        public void endElement(String tag) {
            level--;
        }

        private Class loadClass(String className)
            throws SAXParseException {
            String msg = null;
            try {
                //load the named class
                Class cls;
                if(classloader==null) {
                    cls=Class.forName(className);
                }
                else {
                    cls=classloader.loadClass(className);
                }
                return cls;
            }
            catch (ClassNotFoundException cnfe) {
                msg = "Class " + className + " cannot be found";
                if (onerror.getIndex() == FAIL)
                    throw new SAXParseException(msg, locator, cnfe);
            }
            catch (NoClassDefFoundError ncdfe) {
                msg = "Class " + className + " cannot be loaded";
                if (onerror.getIndex() == FAIL)
                    throw new SAXParseException(msg, locator);
            }

            if (onerror.getIndex() == REPORT) {
                project.log(msg, project.MSG_WARN);
            }
            else {
                project.log(msg, project.MSG_DEBUG);
            }
            return null;
        }

    //end inner class AntLibraryHandler
    }


    /**
     * this class is used for alias elements
     *
     * @author slo
     * @created 11 November 2001
     */
    public static class Alias {
        /**
         * Description of the Field
         */
        private String name;
        /**
         * Description of the Field
         */
        private String as;


        /**
         * Sets the Name attribute of the Alias object
         *
         * @param name The new Name value
         */
        public void setName(String name) {
            this.name = name;
        }


        /**
         * Sets the As attribute of the Alias object
         *
         * @param as The new As value
         */
        public void setAs(String as) {
            this.as = as;
        }
    //end inner class alias
    }

//end class Antlib
}


