/*
 * 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.xml.sax.*;
import javax.xml.parsers.*;

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

/**
 * 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 <a href="j_a_fernandez@yahoo.com">Jose Alberto Fernandez</a>
 */
public class Antlib extends Task implements DeclaringTask {

    /** Location of destriptor in library */
    public static String XML_DESCRIPTOR =    "META-INF/antlib.xml";

    /** Prefix name for DTD of descriptor */
    public static String ANTLIB_DTD_PREFIX = "Antlib-V";
    public static String ANTLIB_DTD_EXT =    ".dtd";

    private String library = null;
    private File file = null;
    private boolean override = false;
    private boolean onCurrent = false;

    private SAXParserFactory saxFactory;
    private Vector aliases = new Vector();

    public class Alias {
	private String name;
	private String as;

	public void setName(String name) {
	    this.name = name;
	}

	public void setAs(String as) {
	    this.as = as;
	}
    }

    public Antlib() {
	super();
	saxFactory  = SAXParserFactory.newInstance();
	saxFactory.setValidating(true);
    }

    public Antlib(Project p) {
	this();
	setProject(p);
    }

    /** 
     * Set name of library to load. The library is located in $ANT_HOME/lib.
     * @param lib the name of library relative to $ANT_HOME/lib.
     */
    public void setLibrary(String lib) {
	this.library = 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 whether to override any existing definitions.
     * @param override if true new definitions will replace existing ones.
     */
    public void setOverride(boolean override) {
	this.override = override;
    }

    /**
     * Set whether to use a new classloader or not. 
     * Default is <code>false</code>.
     * This property is mostly used by the core when loading core tasks.
     * @param onCurrent if true the current classloader will be used to
     *                  load the definitions.
     */
    public void setOnCurrent(boolean onCurrent) {
	this.onCurrent = onCurrent;
    }

    /** Create new Alias element. */
    public Alias createAlias() {
	Alias als = new Alias();
	aliases.add(als);
	return als;
    }

    public void execute() throws BuildException {
	File realFile = file;
	if (library != null) {
	    if (file != null) {
		String msg = "You cannot specify both file and library.";
		throw new BuildException(msg, location);
	    }
	    // For the time being libraries live in $ANT_HOME/lib.
	    // The idea being that we would not load all the jar there anymore
	    String home = project.getProperty("ant.home");
	    
	    if (home == null)
		throw new BuildException("ANT_HOME not set as required.");

	    realFile = new File(new File(home, "lib"), library);
	}
	else if (file == null) {
	    String msg = "Must specify either library or file attribute.";
	    throw new BuildException(msg, location);
	}
	if (!realFile.exists()) {
	    String msg = "Cannot find library: " + realFile;
	    throw new BuildException(msg, location);
	}

	InputStream is = getDescriptor(realFile);
	
	if (is == null) {
	    String msg = "Missing descriptor on library: " + realFile;
	    throw new BuildException(msg, location);
	}

	evaluateDescriptor(onCurrent? null : makeClassLoader(realFile), 
			   processAliases(), is);
    }

    /**
     * Load definitions directly from an external XML file.
     * @param xmlfile XML file in the Antlib format.
     */
    public void loadDefinitions(File xmlfile) throws BuildException {
	try {
	    InputStream is = new FileInputStream(xmlfile);
	    loadDefinitions(is);
	}
	catch (IOException io) {
	    throw new BuildException("Cannot read file: " + file, io);
	}
    }

    /**
     * Load definitions directly from InputStream.
     * @param is InputStream for the Antlib descriptor.
     */
    public void loadDefinitions(InputStream is) throws BuildException {
	evaluateDescriptor(null, processAliases(), is);
    }

    private InputStream getDescriptor(File file) {
	try {
	    final ZipFile zipfile = new ZipFile(file);
	    ZipEntry entry = zipfile.getEntry(XML_DESCRIPTOR);
	    
	    if (entry == null) return null;
	    
	    // Guarantee that when Entry is close so does the zipfile instance.
	    return new FilterInputStream(zipfile.getInputStream(entry)){
		public void close() throws IOException {
		    super.close();
		    zipfile.close();
		}
	    };
	}
	catch(ZipException ze) {
	    throw new BuildException("Not a library file.", ze, location);
	}
	catch(IOException ioe) {
	    throw new BuildException("Cannot read library content.",
				     ioe, location);
	}
    }

    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;
    }

    protected ClassLoader makeClassLoader(File fl) throws BuildException {
	Path clspath = new Path(project);
	clspath.setLocation(fl);
	AntClassLoader al = new AntClassLoader(project, clspath, true);
	return al;
    }

    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(XML_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.
     */
    private class AntLibraryHandler extends HandlerBase {
	
	private final ClassLoader cl;
	private final Properties aliasMap;
	private Locator locator = null;

	AntLibraryHandler(ClassLoader cl, Properties als) {
	    this.cl = cl;
	    this.aliasMap = als;
	}

        public void setDocumentLocator(Locator locator) {
            this.locator = locator;
        }

	public void startElement(String tag, AttributeList attrs)
	    throws SAXParseException 
	{
	    if ("antlib".equals(tag)) {
		// No attributes to worry about
		return;
	    }
	    if ("task".equals(tag) || "type".equals(tag)) {
		String name = null;
		String className = 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 {
			throw new SAXParseException("Unexpected attribute \"" 
						    + key + "\"", locator);
		    }
		}
		if (name == null || className == null) {
		    String msg = "Underspecified " + tag + " declaration.";
		    throw new SAXParseException(msg, locator);
		}
		
		try {
		    String alias = aliasMap.getProperty(name);

		    if (alias != null) name = alias;
		    if (!override && inUse(name)) {
			String msg = "Cannot override " + tag + ": " + name;
			log(msg, Project.MSG_WARN);
			return;
		    }

		    Class cls = (cl != null)? 
			cl.loadClass(className) : Class.forName(className);

		    if (tag.equals("task")) {
			project.addTaskDefinition(name, cls);
		    } else {
			project.addDataTypeDefinition(name, cls);
		    }
		} catch (ClassNotFoundException cnfe) {
		    String msg = "Class " + className +
			" cannot be found";
		    throw new SAXParseException(msg, locator, cnfe);
		} catch (NoClassDefFoundError ncdfe) {
		    String msg = "Class " + className +
			" cannot be found";
		    throw new SAXParseException(msg, locator);
		}
	    } 
	    else {
		throw new SAXParseException("Unexpected element \"" + 
					    tag + "\"", 
					    locator);
	    }
	}

	private boolean inUse(String name) {
	    return (project.getTaskDefinitions().get(name) != null ||
		    project.getDataTypeDefinitions().get(name) != null);
	}

	/**
	 * Recognizes the DTD declaration for antlib and returns
	 * the corresponding DTD definition from a resource.
	 * <P>
	 * To allow for future versions of the DTD format it will
	 * search for any DTDs of the form "Antlib-V.*\.dtd".
	 */
        public InputSource resolveEntity(String publicId,
                                         String systemId) {
        
	    if (systemId != null &&
		systemId.startsWith(ANTLIB_DTD_PREFIX) &&
		systemId.endsWith(ANTLIB_DTD_EXT)) {
		InputSource is =
		    new InputSource(getClass().getResourceAsStream(systemId));

		is.setSystemId(systemId);
		return is;
	    }
	    return null;
	}
    }
}


