/* ======================================================================
   Parts Copyright 2006 University of Leeds, Oxford University, University of the Highlands and Islands.

   Licensed 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

       http://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.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 );
    }
}
