/*
 * 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", "Tomcat", 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.beans.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.*;
import java.util.*;
import org.xml.sax.SAXException;
import org.w3c.dom.*;
import org.apache.tools.ant.taskdefs.*;

/**
 * Configures a Project (complete with Targets and Tasks) based on
 * a XML build file.
 *
 * @author duncan@x180.com
 */

public class ProjectHelper {

    public static void configureProject(Project project, File buildFile)
        throws BuildException
    {

        // XXX
        // need to get rid of the DOM layer and use SAX

        Document doc;

        try {
            doc=Parser.getParser(project).parse(buildFile);
        } catch (IOException ioe) {
            String msg = "Can't open config file: " + buildFile +
                " due to: " + ioe;
            throw new BuildException(msg);
        } catch (SAXException se) {
            String msg = "Can't open config file: " + buildFile +
                " due to: " + se;
            throw new BuildException(msg);
        }

        Element root = doc.getDocumentElement();

        // sanity check, make sure that we have the right element
        // as we aren't validating the input

        if (!root.getTagName().equals("project")) {
            String msg = "Config file is not of expected XML type";
            throw new BuildException(msg);
        }

        project.setDefaultTarget(root.getAttribute("default"));

        String name = root.getAttribute("name");
        project.setName(name);
        if (name != null) project.addReference(name, project);

        String id = root.getAttribute("id");
        if (id != null) project.addReference(id, project);

        String baseDir = project.getProperty("basedir");
        if (baseDir == null) {
            baseDir = root.getAttribute("basedir");
            if (baseDir.equals("")) {
                // Using clunky JDK1.1 methods here
                baseDir = new File(buildFile.getAbsolutePath()).getParent();
            }
        }
        project.setBasedir(baseDir);

        // set up any properties that may be in the config file

        //      configureProperties(project, root);

        // set up any task defs that may be in the config file

        //      configureTaskDefs(project, root);

        // set up the taskdefs, properties, and targets into the project
        configureProject(project, root);
    }

    private static void configureProject(Project project, Element root)
        throws BuildException
    {
        // configure taskdefs
        NodeList list = root.getElementsByTagName("taskdef");
        for (int i = 0; i < list.getLength(); i++) {
            Task taskdef = new Taskdef();
            configure(project, taskdef, (Element)list.item(i));
            taskdef.setProject(project);
            taskdef.init();
        }

        // configure properties
        list = root.getElementsByTagName("property");
        for (int i = 0; i < list.getLength(); i++) {
            Task property = new Property();
            configure(project, property, (Element)list.item(i));
            property.setProject(project);
            property.init();
        }

        // configure targets
        list = root.getElementsByTagName("target");
        for (int i = 0; i < list.getLength(); i++) {
            Element element = (Element)list.item(i);
            String targetName = element.getAttribute("name");
            String targetDep = element.getAttribute("depends");
            String targetCond = element.getAttribute("if");
            String targetId = element.getAttribute("id");

            // all targets must have a name
            if (targetName.equals("")) {
                String msg = "target element appears without a name attribute";
                throw new BuildException(msg);
            }

            Target target = new Target();
            target.setName(targetName);
            target.setCondition(targetCond);
            project.addTarget(targetName, target);

            if (targetId != null && !targetId.equals("")) 
                project.addReference(targetId,target);

            // take care of dependencies

            if (targetDep.length() > 0) {
                StringTokenizer tok =
                    new StringTokenizer(targetDep, ",", false);
                while (tok.hasMoreTokens()) {
                    target.addDependency(tok.nextToken().trim());
                }
            }

            // populate target with tasks

            configureTasks(project, target, element);
        }
    }

    private static void configureTasks(Project project,
                                       Target target,
                                       Element targetElement)
        throws BuildException
    {
        NodeList list = targetElement.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);

            // right now, all we are interested in is element nodes
            // not quite sure what to do with others except drop 'em

            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element element = (Element)node;
                String taskType = element.getTagName();

                // XXX
                // put in some sanity checking

                Task task = project.createTask(taskType);

                // get the attributes of this element and reflect them
                // into the task

                configure(project, task, element);
                task.init();
                task.setTarget(target);
                target.addTask(task);

                processNestedProperties(project, task, element);
            }
        }
    }

    private static void processNestedProperties(Project project,
                                                Object target,
                                                Element targetElement)
        throws BuildException
    {
        Class targetClass = target.getClass();
        NodeList list = targetElement.getChildNodes();

        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);

            // right now, all we are interested in is element nodes
            // not quite sure what to do with others except drop 'em

            if (node.getNodeType() == Node.TEXT_NODE) {
                String text = ((Text)node).getData();
                try {
                    Method addProp = targetClass.getMethod(
                        "addText", new Class[]{"".getClass()});
                    Object child = addProp.invoke(target, new Object[] {text});
                } catch (NoSuchMethodException nsme) {
                    if (text.trim().length() > 0)
                        throw new BuildException(targetClass + 
                            " does not support nested text elements");
                } catch (InvocationTargetException ite) {
                    throw new BuildException(ite.getMessage());
                } catch (IllegalAccessException iae) {
                    throw new BuildException(iae.getMessage());
                }
            }

            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element element = (Element)node;
                String propType = element.getTagName();
                String methodName = "create" +
		    Character.toUpperCase(propType.charAt(0)) +
                    propType.substring(1);

                try {
                    Method addProp =
                        targetClass.getMethod(methodName, new Class[]{});
                    Object child = addProp.invoke(target, new Object[] {});

                    configure(project, child, element);

                    processNestedProperties(project, child, element);
                } catch (NoSuchMethodException nsme) {
                    throw new BuildException(targetClass + 
                        " does not support nested " + propType + " properties");
                } catch (InvocationTargetException ite) {
                    throw new BuildException(ite.getMessage());
                } catch (IllegalAccessException iae) {
                    throw new BuildException(iae.getMessage());
                }

            }
        }
    }

    private static void configure(Project project,
                                  Object target,
                                  Element element)
        throws BuildException
    {
        NamedNodeMap nodeMap = element.getAttributes();

        if( target instanceof TaskAdapter )
            target=((TaskAdapter)target).getProxy();

        // XXX
        // instead of doing this introspection each time around, I
        // should have a helper class to keep this info around for
        // each kind of class

        Hashtable propertySetters = new Hashtable();
        BeanInfo beanInfo;
        try {
            beanInfo = Introspector.getBeanInfo(target.getClass());
        } catch (IntrospectionException ie) {
            String msg = "Can't introspect class: " + target.getClass();
            throw new BuildException(msg);
        }

        PropertyDescriptor[] pda = beanInfo.getPropertyDescriptors();
        for (int i = 0; i < pda.length; i++) {
            PropertyDescriptor pd = pda[i];
            String property = pd.getName();
            Method setMethod = pd.getWriteMethod();
            if (setMethod != null) {

                // make sure that there's only 1 param and that it
                // takes a String object, all other setMethods need
                // to get screened out

                Class[] ma =setMethod.getParameterTypes();
                if (ma.length == 1) {
                    Class c = ma[0];
                    if (c.getName().equals("java.lang.String")) {
                        propertySetters.put(property, setMethod);
                    }
                }
            }
        }

        for (int i = 0; i < nodeMap.getLength(); i++) {
            Node node = nodeMap.item(i);

            // these should only be attribs, we won't see anything
            // else here.

            if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
                Attr attr = (Attr)node;

                // reflect these into the target

                Method setMethod = (Method)propertySetters.get(attr.getName());
                if (setMethod == null) {
                    if (attr.getName().equals("id")) {
                        project.addReference(attr.getValue(), target);
                        continue;
                    }

                    String msg = "Configuration property \"" + attr.getName() +
                        "\" does not have a setMethod in " + target.getClass();
                    throw new BuildException(msg);
                }

                String value=replaceProperties(  attr.getValue(), project.getProperties() );
                try {
                    setMethod.invoke(target, new String[] {value});
                } catch (IllegalAccessException iae) {
                    String msg = "Error setting value for attrib: " +
                        attr.getName();
                    iae.printStackTrace();
                    throw new BuildException(msg);
                } catch (InvocationTargetException ie) {
                    String msg = "Error setting value for attrib: " +
                        attr.getName() + " in " + target.getClass().getName();
                    ie.printStackTrace();
                    ie.getTargetException().printStackTrace();
                    throw new BuildException(msg);
                }
            }
        }
    }

    /** Replace ${NAME} with the property value
     */
    public static String replaceProperties( String value, Hashtable keys )
        throws BuildException
    {
        // XXX use Map instead of proj, it's too heavy

        // XXX need to replace this code with something better.
        StringBuffer sb=new StringBuffer();
        int i=0;
        int prev=0;
        // assert value!=nil
        int pos;
        while( (pos=value.indexOf( "$", prev )) >= 0 ) {
            if(pos>0)
                sb.append( value.substring( prev, pos ) );
            if( value.charAt( pos + 1 ) != '{' ) {
                sb.append( value.charAt( pos + 1 ) );
                prev=pos+2; // XXX
            } else {
                int endName=value.indexOf( '}', pos );
                if( endName < 0 ) {
                    throw new BuildException("Syntax error in prop: " +
                                             value );
                }
                String n=value.substring( pos+2, endName );
                String v=(String) keys.get( n );
                //System.out.println("N: " + n + " " + " V:" + v);
                sb.append( v );
                prev=endName+1;
            }
        }
        if( prev < value.length() ) sb.append( value.substring( prev ) );
        //      System.out.println("After replace: " + sb.toString());
        // System.out.println("Before replace: " + value);
        return sb.toString();
    }
}









