/* ======================================================================
The Bodington System Software License, Version 1.0
  
Copyright (c) 2001 The University of Leeds.  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 acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.

4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.

5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.

THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES 
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO 
EVENT SHALL THE UNIVERSITY OF LEEDS 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 was originally created by the University of Leeds and may contain voluntary 
contributions from others.  For more information on the Bodington Open Source Project, please 
see http://bodington.org/

====================================================================== */

package org.bodington.ant;

import java.io.File;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.bodington.servlet.template.XmlTemplate;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * An ANT task for performing template compilation. The formatting of error
 * messages has been influenced by the style adopted by the <a
 * href="http://jikes.sourceforge.net/">Jikes</a> java compiler.
 * <h3>Use</h3>
 * <p>
 * As this task is not a core ANT task, you will need to declare it in your
 * build file. You will need to add something such as the following (the nested
 * <code>classpath</code> element tells ANT where to find the task).
 * </p>
 * 
 * <pre>
 * 
 *      &lt;taskdef name=&quot;template-compiler&quot; classname=&quot;org.bodington.ant.TemplateBuilderTask&quot;&gt;
 *        &lt;classpath&gt;
 *          &lt;pathelement location=&quot;${build}/bodserver.jar&quot;/&gt;
 *        &lt;/classpath&gt;
 *      &lt;/taskdef&gt;
 *  
 * </pre>
 * 
 * <h3>Parameters</h3>
 * <table cellspacing="0" border="1"> <thead>
 * <tr>
 * <th>Attribute</th>
 * <th>Description</th>
 * <th>Required</th>
 * </tr>
 * </thead> <tbody>
 * <tr>
 * <td>srcdir</td>
 * <td>the source directory of the (HTML) templates.</td>
 * <td>Yes</td>
 * </tr>
 * <tr>
 * <td>destdir</td>
 * <td>the output directory for the template class files.</td>
 * <td>Yes</td>
 * </tr>
 * <tr>
 * <td>classpath</td>
 * <td>the CLASSPATH to be used by the underlying task. </td>
 * <td>Yes (or <i>classpathref</i>)</td>
 * </tr>
 * <tr>
 * <td>classpathref</td>
 * <td>a reference to the CLASSPATH required by the underlying task. See the
 * example below. </td>
 * <td>Yes (or <i>classpath</i>)</td>
 * </tr>
 * <tr>
 * <td>xmlwarn</td>
 * <td>sets whether or not XML parsing errors and warnings in the templates are
 * reported; defaults to <code>true</code> (see
 * {@link org.xml.sax.ErrorHandler}).</td>
 * <td>No</td>
 * </tr>
 * <tr>
 * <td>failonerror</td>
 * <td>Indicates whether the build should stop if a template contains
 * unrecoverable errors; defaults to <code>true</code>.</td>
 * <td>No</td>
 * </tr>
 * <tr>
 * <td>templatedir</td>
 * <td><b>Deprecated:</b> <i>use srcdir instead.</i> </td>
 * <td>Yes</td>
 * </tr>
 * <tr>
 * <td>classesdir</td>
 * <td><b>Deprecated:</b> <i>use destdir instead.</i> </td>
 * <td>Yes</td>
 * </tr>
 * <tr>
 * <td>buildpath</td>
 * <td><b>Deprecated:</b> <i>use classpath instead.</i> </td>
 * <td>Yes</td>
 * </tr>
 * <tr>
 * <td>buildpathref</td>
 * <td><b>Deprecated:</b> <i>use classpathref instead.</i> </td>
 * <td>Yes</td>
 * </tr>
 * </tbody> </table>
 * <h3>Parameters specified as nested elements</h3>
 * This task forms an implicit <code>FileSet</code> and supports all
 * attributes of <code>&lt;fileset&gt;</code> (<code>dir</code> becomes
 * <code>srcdir</code>) as well as the nested <code>&lt;include&gt;</code>,
 * <code>&lt;exclude&gt;</code> and <code>&lt;patternset&gt;</code>
 * elements. If no pattern is specified, then the default is to include all
 * <code>*.html</code> files found under the directory specified by the
 * <code>srcdir</code> attribute.
 * <h3>Examples</h3>
 * After having declared the task with a <task>taskdef</code>, you can declare
 * the <i>path-like structure </i> to which your <code>classpathref</code>
 * refers in the following way.
 * 
 * <pre>
 * 
 *      &lt;path id=&quot;template.build.path&quot;&gt;
 *          &lt;pathelement location=&quot;${java.home}/lib/tools.jar&quot;/&gt;
 *          &lt;pathelement location=&quot;${build.bodington.classes}&quot;/&gt;
 *          &lt;pathelement location=&quot;${tomcat-4}/common/lib/servlet.jar&quot;/&gt;
 *      &lt;/path&gt; 
 *  
 * </pre>
 * 
 * You can then call the task (within a target) using the following entry:
 * 
 * <pre>
 * 
 *      &lt;template-compiler srcdir=&quot;${build.bodington}/templates&quot;
 *          destdir=&quot;${build.bodington}/WEB-INF/template_classes&quot;
 *          classpathref=&quot;template.build.path&quot;/&gt;
 *  
 * </pre>
 * 
 * The following restricts the templates that get compiled, to those called
 * <code>main.html</code>, wherever they appear in the directory tree under
 * the directory specified by <code>srcdir</code>.
 * 
 * <pre>
 * 
 *      &lt;template-compiler srcdir=&quot;${build.bodington}/templates&quot;
 *          destdir=&quot;${build.bodington}/WEB-INF/template_classes&quot;
 *          classpathref=&quot;template.build.path&quot;&gt;
 *          &lt;include name=&quot;**&#47;main.html&quot; /&gt;
 *      &lt;/template-compiler&lt;
 *  
 * </pre>
 * 
 * @author Alexis O'Connor
 * @see org.xml.sax.ErrorHandler
 * @see <a href="http://ant.apache.org/">Apache ANT </a>
 * @see <a href="http://jikes.sourceforge.net/">Jikes</a>
 */
public class TemplateBuilderTask extends MatchingTask
{
    /**
     * Helper class to handle XML parsing errors and warnings.
     */
    private static class ErrorReporter implements ErrorHandler
    {
        private Vector messages = new Vector();
        private String currentFile = null;
        private boolean warnings = false;
        private SAXParseException fatalError = null;
        
        public void error( SAXParseException exception ) throws SAXException
        {
            if (warnings)
                logProblem(exception, "<xml:err>");
        }
    
        public void fatalError( SAXParseException fatalError ) throws SAXException
        {
            this.fatalError = fatalError;
        }
    
        public void warning( SAXParseException exception ) throws SAXException
        {
            if (warnings)
                logProblem(exception, "<xml:warn>");
        }
        
        public void setWarnings(boolean warnings)
        {
            this.warnings = warnings;
        }
        
        private void logProblem(SAXParseException e, String type)
        {
            Vector oldMsgs = null;
            String oldFile = null;
            // Is this a new file? (if so, reset state).
            if (!e.getSystemId().equals(currentFile))
            {
                oldFile = currentFile;
                oldMsgs = messages;
                currentFile = e.getSystemId();
                fatalError = null;
                messages = new Vector();
            }
            messages.add(type + " " + e.getLineNumber() + ". " + e.getMessage());
            if (oldFile != null)
                reportProblems(oldFile, oldMsgs);
        }
        
        private void reportProblems(String fileName, Vector messages)
        {
            String problems = (messages.size() > 1) ? "problems" : "problem";
            System.err.println("Found " + messages.size() + " " + problems 
                + " with \"" + fileName + "\":");
            Enumeration e = messages.elements();
            while (e.hasMoreElements())
                System.err.println(e.nextElement());
        }
        
        SAXParseException getFatalError()
        {
            return fatalError;
        }
    }

    private String classpath;
    private File srcdir;
    private File destdir;
    private boolean xmlWarn = true;
    private boolean failonerror = true;

    /**
     * Set the value of the <code>classpath</code> attribute via a reference.
     * @param classpathref the value to be set.
     */
    public void setClasspathRef(Reference classpathref)
    {
        Path path = new Path(getProject());
        path.setRefid(classpathref);
        this.classpath = path.toString();
    }
    
    /**
     * Set the value of the <code>buildpath</code> attribute via a reference.
     * @param buildpathref the value to be set.
     * @deprecated use {@link #setClasspathRef(Reference)} instead.
     */
    public void setBuildpathRef(Reference buildpathref)
    {
        setClasspathRef(buildpathref);
    }
    
    /**
     * Set the value of the <code>classpath</code> attribute.
     * @param classpath the value to be set.
     */
    public void setClasspath(String classpath)
    {
        this.classpath = classpath;
    }
    
    /**
     * Set the value of the <code>buildpath</code> attribute.
     * @param buildpath the value to be set.
     * @deprecated use {@link #setClasspath(String)} instead.
     */
    public void setBuildpath(String buildpath)
    {
        setClasspath(buildpath);
    }

    /**
     * Set the value of the <code>srcdir</code> attribute.
     * @param srcdir the value to be set.
     */
    public void setSrcdir(File srcdir)
    {
        this.srcdir = srcdir;
    }
    
    /**
     * Set the value of the <code>templatedir</code> attribute.
     * @param templatedir the value to be set.
     * @deprecated use {@link #setSrcdir(File)} instead.
     */
    public void setTemplatedir(File templatedir)
    {
        setSrcdir(templatedir);
    }

    /**
     * Set the value of the <code>destdir</code> attribute.
     * @param destdir the value to be set.
     */
    public void setDestdir(File destdir)
    {
        this.destdir = destdir;
    }
    
    /**
     * Set the value of the <code>templatedir</code> attribute.
     * @param templatedir the value to be set.
     * @deprecated use {@link #setDestdir(File)} instead.
     */
    public void setTemplateclasses(File templatedir)
    {
        setDestdir(templatedir);
    }
    
    /**
     * Set the value of the <code>xmlwarn</code> attribute. If 
     * <code>true</code>, errors and warnings in the XML templates will be 
     * reported (default: <code>true</code>). 
     * @param xmlWarn the value to be set.
     */
    public void setXmlWarn(boolean xmlWarn)
    {
        this.xmlWarn = xmlWarn;
    }
    
    /**
     * Set the value of the <code>failonerror</code> attribute. If 
     * <code>true</code>, the build as a whole will be halted if there are any
     * errors in the templates (default: <code>true</code>). 
     * @param failonerror the value to be set.
     */
    public void setFailOnError(boolean failonerror)
    {
        this.failonerror = failonerror;
    }
    
    protected void checkParameters() throws BuildException
    {
        // Mandatory arguements present?
        if ( classpath == null )
            throw new BuildException(
                "\"classpath\" property has not been set." );
        if ( destdir == null )
            throw new BuildException(
                "\"destdir\" property has not been set." );
        if ( srcdir == null )
            throw new BuildException(
                "\"srcdir\" property has not been set." );

        // Valid parameters?
        if ( !srcdir.exists() || !srcdir.isDirectory() )
            throw new BuildException( "Template directory not found: "
                + srcdir.getPath() );

        if ( !destdir.exists() || !destdir.isDirectory() )
            throw new BuildException( "Template directory not found: "
                + destdir.getPath() );
    }
    
    protected int iterateTemplates( ErrorReporter errorReporter,
        PrintWriter errorWriter, boolean compile )
    {
        FileSet fs = getImplicitFileSet();
        if (!fs.hasPatterns() && !fs.hasSelectors())
            fs.setIncludes("**/*.html");
        fs.setDir(srcdir);
        DirectoryScanner ds = fs.getDirectoryScanner(getProject());
        String[] files = ds.getIncludedFiles();
        int templateCount = 0;
        int errorCount = 0;
        for (int i = 0; i < files.length; i++)
        {
            XmlTemplate template = new XmlTemplate( srcdir, 
                new File(srcdir, files[i]), destdir, classpath );
            template.setTimestampLatency( 0 ); // design-time so none!
            templateCount += template.needsCompiling() ? 1 : 0;
            if ( compile )
            {
                template.setErrorHandler( errorReporter );
                template.setErrorWriter( errorWriter );
                try
                {
                    if ( template.compile() 
                        != XmlTemplate.JAVA_COMPILE_SUCCESS )
                    {
                        throw new BuildException("Error(s) compiling "
                            + "template (java -> bytecode)." );
                    }
                    if (errorReporter.getFatalError() != null)
                        throw errorReporter.getFatalError();
                }
                catch ( SAXParseException e )
                {
                    // Most likely a problem parsing the XML file:
                    ++errorCount;
                    --templateCount;
                    System.err.println( "Error(s) found with \""
                        + e.getSystemId() + "\":" );
                    System.err.println( e.getLineNumber() + ". "
                        + e.getMessage() );
                    if (failonerror)
                        throw new BuildException(e);
                }
                catch ( Exception e )
                {
                    // Most likely a problem with the XML -> java stage:
                    ++errorCount;
                    --templateCount;
                    System.err.println( "Error(s) found with \""
                        + files[i] + "\":" );
                    System.err.println( e.getMessage() );
                    if (failonerror)
                        throw new BuildException(e);
                }
            }
        }
        if (errorCount > 0)
            System.err.println("*** WARNING: " + errorCount + " template(s) "
                + "contain unrecoverable errors. ***");
        return templateCount;
    }
    
    /**
     * Execute the task.
     * @see org.apache.tools.ant.Task#execute()
     */
    public void execute()
    {
        checkParameters();
        // 1st time round, count how many templates needs compiling:
        int templateCount = iterateTemplates( null, null, false );
        String templates = (templateCount > 1) ? "templates" : "template";
        if ( templateCount > 0 )
            System.out.println( "Compiling " + templateCount + " " + templates
                + " to " + destdir );
        else
            return;
        // 2nd time round, compile the templates:
        ErrorReporter errorReporter = new TemplateBuilderTask.ErrorReporter();
        errorReporter.setWarnings( xmlWarn );
        PrintWriter errorWriter = new PrintWriter( System.err );
        iterateTemplates( errorReporter, errorWriter, true );
    }
}
