/* ======================================================================
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.servlet.template;

import org.bodington.servlet.*;
import org.bodington.servlet.facilities.*;
import org.bodington.xml.ClasspathEntityResolver;

import java.io.*;
import java.lang.reflect.*;
import java.net.URI;
import java.util.Hashtable;
import java.util.Map;

import javax.servlet.*;
import javax.servlet.http.*;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;


import com.sun.tools.javac.Main;



/**
 * Class that encapsulates the process of template compilation. This class
 * handles both the newer (XML) style of template and acts as a wrapper to the
 * legacy ({@link org.bodington.servlet.template.LegacyTemplate}) style too.
 * This class encapsulates references to the template source (<code>.html</code>
 * file) and its corresponding compiled form (<code>.class</code> file). This
 * class performs the compilation, handling the dependancies based on file
 * timestamps, itself.
 * 
 * At the moment this class has an unnecessary dependacy on ServletException 
 * which should be removed so that is can exist cleanly outside a servlet container.
 *  
 * @author bmb6jrm
 * @see org.bodington.servlet.template.LegacyTemplate
 */
public class XmlTemplate
{
    private static final String[] expression_tag_names=
    {
	"literal",
	"variable",
	"assign",
	"call",
	"class"
    };
    
    private static final String[] htmlfragment_tag_names=
    {
	"htmlfragment"
    };
    
    private static final String[] call_tag_names=
    {
	"target",
	"parameters"
    };
    
    private static boolean getDoneCompilerOutputCheck=false;
    private static boolean getCanGetCompilerOutput = false;
    
    // class loaders hashed against class directories
    private static Hashtable class_loaders = new Hashtable();
    
    
    
    private static DocumentBuilderFactory docbuilderfactory;
    private static org.xml.sax.EntityResolver entity_resolver;
    static
    {
	docbuilderfactory = DocumentBuilderFactory.newInstance();
	docbuilderfactory.setValidating( true );
	entity_resolver = new ClasspathEntityResolver(XmlTemplate.class);
    }
    
    
    XmlTemplateClassLoader class_loader;
    XmlTemplateProcessor processor;
    
    File xml_source;
    File class_dir;
    String class_name;
    String class_path;
    
    // indicates the timestamp of the version of the xml file that was
    // last compiled
    long compiled_xml_source_timestamp = -1;
    
    boolean compiling;
    
    //Hashtable attribute_tables = new Hashtable();
    PrintWriter writer=null;
    boolean preparation_done;
    String html_body_class=null;
    Hashtable template_variable_class_table;
    
    
    
    /**
     * Create an instance of this class.
     * @param xml_source the absolute path to the template source file.
     * @param class_dir the destination directory of the compiled class file.
     * @param class_name the name of the compiled class file.
     * @param class_path the <code>CLASSPATH</code> to be used for the template 
     * java -> bytecode compilation stage.
     */
    public XmlTemplate( File xml_source, File class_dir, String class_name, String class_path  )
    {
	this.xml_source = xml_source;
	this.class_dir = class_dir;
	this.class_name = class_name;
	this.class_path = class_path;
	processor = null;
	compiling = false;
	
	
	class_loader = getSuitableClassLoader();
	File class_file = class_loader.getClassFile( class_name );

    // We note the compiled XML timestamp in case it changes within the lifespan
    // of this object.
	if ( class_file !=null && xml_source!=null && xml_source.exists() )
		this.compiled_xml_source_timestamp = xml_source.lastModified();
    }
    
    /**
     * Create an instance of this class. This version of the constructor, uses
     * the method {@link #templateToClassName(String)} to generate the class
     * name, by relativizing the template name against the value of the
     * <code>srcdir</code> argument.
     * @param srcdir the base directory containing the template source
     * @param xml_source the absolute path to the template source file.
     * @param class_dir the destination directory of the compiled class file.
     * @param class_path the <code>CLASSPATH</code> to be used for the
     *        template java -> bytecode compilation stage.
     * @see #XmlTemplate(File, File, String, String)
     */
    public XmlTemplate( File srcdir, File xml_source, File class_dir, 
        String class_path  )
    {
        this( xml_source, class_dir, templateToClassName( relativize( srcdir,
            xml_source ) ), class_path );
    }
    
    /**
     * Relativize one file against the other. The first argument is the base
     * directory and the second is the file to be relativized against it.
     * @param baseDir the base directory to be relativized against.
     * @param file the file to be relativized.
     * @return the second argument in relativized form.
     * @see java.net.URI
     */
    protected static String relativize( File baseDir, File file )
    {
        URI baseURI = baseDir.toURI();
        URI uri = baseURI.relativize( file.toURI() );
        return uri.toString();
    }
    
    /**
     * Create a class name for the specified template name. This method expects
     * the argument to be in <i>relative URI form</i>. An example of this would
     * be <code>style_default/default/index.html</code>. 
     * @param template the path to the template (in relative URI form).
     * @return the corresponding class name.
     */
    protected static String templateToClassName( String template )
    {
        StringBuffer sb = new StringBuffer( template );
        // prepend "working.":
        sb.insert( 0, "working." );
        // replace first "/" with "_":
        int i = sb.indexOf( "/" );
        sb.replace( i, i + 1, "_" );
        // replace last "/" with ".template_":
        i = sb.lastIndexOf( "/" );
        sb.replace( i, i + 1, ".template_" );
        // replace all "-" with "_":
        while ( (i = sb.indexOf( "-" )) != -1 )
            sb.replace( i, i + 1, "_" );
        // remove ".html" from end:
        sb.delete( sb.length() - ".html".length(), sb.length() );
        return sb.toString();
    }
    
    private ErrorHandler errorHandler;
    
    /**
     * Set the error handler to be used for this template. This method enables
     * a caller to register a handler for XML template parsing errors and 
     * warnings.
     * @param errorHandler the error handler to be used.
     * @see #getErrorHandler()
     */
    public void setErrorHandler(ErrorHandler errorHandler)
    {
        this.errorHandler = errorHandler;
    }
    
    /**
     * Get the error handler used by this template.
     * @return the error handler used by this template.
     * @see #setErrorHandler(ErrorHandler)
     */
    public ErrorHandler getErrorHandler()
    {
        return errorHandler;
    }
    
    private int timestampLatency = 5;
    
    /**
     * Set the value of the timestampLatency property. When this class carries
     * out a dependancy check to see whether or not the HTML templates needs to
     * be compiled or not, it looks at the timestamps of the HTML file and the
     * corresponding class file. If the HTML file is newer than the class file,
     * this indicates that a compilation is required. The value of this property
     * can be used to add a <i>latency</i> to this dependancy check. For
     * example, with a value of <code>5</code> the HTML file can be upto 5
     * seconds older than the class file, before {@link #needsCompiling()} will
     * return a value of <code>true</code>. At <i>design-time</i> users will
     * probably want to give this property a value of <code>0</code> (for
     * obvious reasons). By default the value of this property is <code>5</code>.
     * <p>
     * <i><B>NOTE:</B> the need for this property arose when deploying on
     * Tomcat v5.5. This is because it was discovered that when deploying in the
     * form of a war, the unpacking algorithm always unpacked the class files
     * before the HTML files. This meant that the HTML templates were always
     * considered to be newer, thus rendering pre-compilation futile! Therefore,
     * this property is set to <code>5</code> by default, but can be
     * overridden if required.</i>
     * @param timestampLatency the new value of the timestampLatency property.
     * @see #needsCompiling()
     * @see #getTimestampLatency()
     */
    public void setTimestampLatency( int timestampLatency )
    {
        this.timestampLatency = timestampLatency;
    }

    /**
     * Get the value of the timestampLatency property (in whole seconds). 
     * @return the value of the timestampLatency property (in whole seconds).
     * @see #setTimestampLatency(int)
     */
    public int getTimestampLatency()
    {
        return timestampLatency;
    }

    /**
     * Indicates whether the template represented by this object need compiling.
     * This method compares the timestamps of the template HTML file and it's
     * corresponding class file. In general, if the timestamp of the HTML file
     * is newer than the timestamp of the class file, then this method returns
     * <code>true</code>.
     * @return <code>true</code> if the template represented by this object
     *         needs compiling, otherwise <code>false</code>.
     * @see #setTimestampLatency(int)
     */
    public synchronized boolean needsCompiling()
    {
    // The HTML file may get deleted within the life-cycle of this object ... 
	if ( !xml_source.exists() )
	    return false;
	
	// needs compiling if the template was replaced with a
	// version that has a different timestamp (older or newer)
	// than one that was loaded earlier in the lifetime of 
	// this Template object
	long xml_source_timestamp = xml_source.lastModified();
	if ( xml_source_timestamp != compiled_xml_source_timestamp )
	    return true;
	
	// needs compiling if the class file doesn't exist
	File class_file = class_loader.getClassFile( class_name );
	if ( class_file==null || !class_file.exists() )
	    return true;
	
	// needs compiling if the class file is older than the
	// source file (taking timestampLatency into account).
	long class_timestamp=class_file.lastModified();
	long latency = getTimestampLatency() * 1000; // whole seconds -> ms.
	return class_timestamp < (xml_source_timestamp - latency);
    }
    
    
    /**
     * Get an object to handle the compilation of this template in a web-app.
     * This method actually triggers the compilation process (if required) of
     * the underlying template. This signature of this method is clearly
     * tightly-coupled to a servlet-driven world, but it can be called from
     * elsewhere in order to drive template compilation. It has code to handle
     * template compilation in a multi-threaded environment. If in a
     * single-threaded environment and / or outside of a servlet-driven world,
     * you can call {@link #compile()} directly instead.
     * @return an object to handle the compilation of this template in a
     *         web-app.
     * @throws SAXParseException if there is some problem parsing an XML
     *         template.
     * @throws IOException if there is some problem parsing a legacy template.
     * @throws ParserConfigurationException if there is some problem configuring
     *         the XML parser.
     * @throws SAXException if there is some other problem with XML parsing.
     * @throws ServletException if there are compilation problems, but delivered
     *         in a form targeted at objects in the servlet world.
     * @throws Exception if there is some other problem (most likely configuring
     *         the java compiler).
     * @see #compile()
     */
    public XmlTemplateProcessor getProcessor() throws SAXParseException, 
        IOException, ParserConfigurationException, SAXException, 
        ServletException, Exception
    {
	if ( !xml_source.exists() && !xml_source.isFile() )
	    return null;
	
    if ( needsCompiling() )
    {
        synchronized ( this )
        {
            // if another thread is updating the class file
            // make do with the current one - don't bother waiting
            if ( processor != null && compiling ) 
                return processor;
            // if there isn't a current processor wait for the
            // compilation to complete
            if ( compiling )
            {
                while ( compiling )
                try
				{wait();} catch ( InterruptedException e )
				{}
                return processor;
            }
            // make sure this flag is set before anyone enters this
            // block
            compiling = true;
        }
        try
        {
            compile();
        	loadProcessor();
        	return processor;
        }
        finally
        {
            // guaranteed to clear the flag
            compiling = false;
            synchronized ( this )
            {
                // allow all the waiting threads to pick up
                // the new processor
                notifyAll();
            }
        }
    }
	
	// doesn't need compiling but may need to be loaded.
	if ( processor == null || class_loader.isClassRecompiled( class_name ) )
	    loadProcessor();
	
	return processor;
    }
    
    private void loadProcessor()
    {
	try
	{
	    Class cl =  class_loader.loadClass( class_name, true );
	    processor = (XmlTemplateProcessor)cl.newInstance();
	}
	catch ( Throwable th)
	{
	    processor = new XmlTemplateErrorProcessor( th.getMessage() );
	}
    }
    
    private PrintWriter error_writer = null;
    private boolean isDefaultErrorWriter = true;
    
    /**
     * Get the object to write java compilation error messages to.
     * @return the object to write java compilation error messages to.
     * @see #isDefaultErrorWriter()
     */
    public PrintWriter getErrorWriter()
    {
        return error_writer;
    }
    
    /**
     * Set the object to write java compilation error messages to.
     * @param error_writer the object to write java compilation error messages 
     * to.
     * @see #isDefaultErrorWriter()
     * @throws NullPointerException if the argument is null.
     */
    public void setErrorWriter(PrintWriter error_writer)
    {
        if (error_writer != null)
        {
            this.error_writer = error_writer;
            isDefaultErrorWriter = false;
        }
        else
            throw new NullPointerException("The argument must be non-null.");
    }
    
    /**
     * Indicates whether this instance is using the default error writer. By
     * default, instances of this class will write any errors from the java
     * compilation stage to a corresponding file. This file will be in the
     * same directory as the generated java file. It will have the same name,
     * but with the suffix <code>.err.txt</code>.
     * @return <code>true</code> if this instance is using the default error
     * writer, otherwise <code>false</code>.
     * @see #setErrorWriter(PrintWriter)
     * @see #getErrorWriter()
     */
    public boolean isDefaultErrorWriter()
    {
        return isDefaultErrorWriter;
    }
    
    /**
     * Compile the XML template.
     * @param xml_document a document representing the XML template.
     * @param directory the directory where the java source code and the binary
     *        class file should be output.
     * @param class_name the name of the template binary class file.
     * @param class_path the CLASSPATH to be used by the java compiler.
     * @return the return value of the java compiler. 
     * @throws ServletException if there are compilation problems, but delivered
     *         in a form targeted at objects in the servlet world.
     * @throws IOException if there is some kind of problem with legacy template
     *         compilation.
     * @throws Exception if there is some other problem (most likely configuring
     *         the java compiler).
     */
    private int compile( Document xml_document, File directory, String class_name, String class_path )
    throws ServletException, IOException, Exception
    {
        if ( !directory.exists() || !directory.isDirectory() )
	    throw new ServletException( "Invalid compilation directory for templates." );
	
	String file_name = class_name.replace( '.', File.separatorChar ) + ".java";
	File java_file = new File( directory, file_name );
	
	File parent_dir = java_file.getParentFile();
	if ( !parent_dir.exists() )
	    parent_dir.mkdirs();

	writer =
	new PrintWriter(
	new OutputStreamWriter(
	new FileOutputStream( java_file ), "US-ASCII" ) );
	
	String unqual_class_name;
	if ( class_name.indexOf( '.' ) > 0 )
	{
	    unqual_class_name = class_name.substring( class_name.lastIndexOf( '.' )+1 );
	    writer.print( "package " );
	    writer.print( class_name.substring( 0, class_name.lastIndexOf( '.' ) ) );
	    writer.println( ";" );
	}
	else
	    unqual_class_name = class_name;
	
	writer.println( "import org.bodington.servlet.*;" );
	writer.println( "import org.bodington.servlet.template.*;" );
	writer.println( "import java.io.*;" );
	writer.println( "import javax.servlet.*;" );
	writer.println( "import javax.servlet.http.*;" );
	
	writer.println( "public class " + unqual_class_name );
	writer.println( " extends XmlTemplateProcessor" );
	writer.println( "{" );
	writer.println( "   public " + unqual_class_name + "()" );
	writer.println( "       {" );
	writer.println( "       }" );
	writer.println( "   public void process( Request request, Response response )" );
	writer.println( "       throws IOException, ServletException" );
	writer.println( "       {" );
	template_variable_class_table = new Hashtable();
	template_variable_class_table.put( "request", Request.class );
	template_variable_class_table.put( "response", Response.class );
	createTemplateVariable( "writer", PrintWriter.class );
	writer.println( "writer=null;" );
	preparation_done = false;
	
        // needed for all HTML templates
        writer.println( "response.disableCaching();" );

	try
	{
	    Element template_element = xml_document.getDocumentElement();
	    if ( !"template".equals( template_element.getTagName() ) )
		throw new IOException( "Invalid template file." );
	    
	    compileElement( template_element );
	    
	    writer.println( "       }" );
	    writer.println( "   }" );
	    writer.close();
	    writer = null;
        
        if (isDefaultErrorWriter())
        {
            File error_log_file = new File( directory, 
                class_name.replace( '.', File.separatorChar ) + ".err.txt" );    
            error_writer = new PrintWriter( new FileWriter( error_log_file ) );
            error_writer.println( "Compilation messages..." );
            error_writer.println( "-classpath " + class_path );
        }
            
        // JDK 1.4.0 does not have the static method 
        // int compile(String[], PrintWriter), so check to see if available and 
        // fall back if not. If this fails don't try again for lifetime of JVM.
        if ( !getDoneCompilerOutputCheck )
        {
            try
            {
                Main.class.getMethod("compile", 
                    new Class[] {new String[0].getClass(), PrintWriter.class});
                getCanGetCompilerOutput = true;
            }
            catch ( Exception e )
            {
                getCanGetCompilerOutput = false;
            }
            finally
            {
                getDoneCompilerOutputCheck = true;
        	}
        }
        
        if (!getCanGetCompilerOutput && isDefaultErrorWriter())
        {
            error_writer.println();
            error_writer.println( "*** NOTE: Compiler errors not available "
                + "because the compiler in your JDK does not have the required "
                + "method. Try upgrading to JDK 1.4.2 / 1.5 ***" );
        }

	    String[] args = null;
	    args = new String[]
	    {
		"-classpath", class_path,
		"-sourcepath", directory.getAbsolutePath(),
		"-d", directory.getAbsolutePath(),
        "-source", "1.4",
		java_file.getAbsolutePath()
	    };
	    
        // NOTE: whilst the below will *run* on J2SDK 1.4.0, it will not 
        // *build*! For that, reflection gymnastics is required.
	    javaCompileStatus = getCanGetCompilerOutput 
            ? Main.compile(args, error_writer) : Main.compile(args);
        if (isDefaultErrorWriter())   
        {
            error_writer.println();
            error_writer.println( "Compilation exit code = " + javaCompileStatus );
        }
	}
	finally
	{
	    if ( writer != null )
	    {
		writer.close();
		writer = null;
	    }
        if ( isDefaultErrorWriter() && error_writer!=null )
            error_writer.close();
	}
    return javaCompileStatus;
    }
    
    /**
     * A constant representing java compile success.
     */
    public static final int JAVA_COMPILE_SUCCESS = 0;
    private int javaCompileStatus = JAVA_COMPILE_SUCCESS;
    
    /**
     * Get the value of the java compile status. This is the value returned by
     * the java compiler (javac) when it last successfully compiled the
     * template. A value equal to {@link #JAVA_COMPILE_SUCCESS} indicates that
     * the template compiled successfully.
     * @return the value of the java compile status.
     * @see #JAVA_COMPILE_SUCCESS
     */
    public int getJavaCompileStatus()
    {
        return javaCompileStatus;
    }
    
    /**
     * Compile this template. This template will only be compiled if it requires
     * compiling (based on the timestamps of the input and output files).
     * <p>
     * There are essentially 3 stages to compiling a template:
     * <ol>
     * <li>parsing the XML and validating it against the DTD.
     * <li>transforming the valid XML into a <code>.java</code> file.
     * <li>compiling the <code>.java</code> file into a <code>.class</code>
     * file.
     * </ol>
     * <p>
     * The process can fail at any stage. For example, stage 2 can generate a
     * <code>.java</code> file that will not compile. This accounts for the
     * numerous exceptions that can get thrown. An entirely successful
     * invocation of this method results in no exceptions being thrown and a
     * return result equal to {@link #JAVA_COMPILE_SUCCESS}.
     * @return the return value of the java compiler.
     * @throws SAXParseException if there is some problem parsing an XML
     *         template.
     * @throws IOException if there is some problem parsing a legacy template.
     * @throws ParserConfigurationException if there is some problem configuring
     *         the XML parser.
     * @throws SAXException if there is some other problem with XML parsing.
     * @throws ServletException if there are compilation problems, but delivered
     *         in a form targeted at objects in the servlet world.
     * @throws Exception if there is some other problem (most likely configuring
     *         the java compiler).
     * @see #JAVA_COMPILE_SUCCESS
     */
    public int compile() throws SAXParseException, IOException,
        ParserConfigurationException, SAXException, ServletException, Exception
    {
        if ( needsCompiling() )
        {
            compiled_xml_source_timestamp = xml_source.lastModified();
            if ( isXml() )
            {
                DocumentBuilder docbuilder = 
                    docbuilderfactory.newDocumentBuilder();
                docbuilder.setEntityResolver( entity_resolver );
                docbuilder.setErrorHandler( errorHandler );
                Document xml_document = docbuilder.parse( xml_source );
                javaCompileStatus = compile( xml_document, class_dir,
                    class_name, class_path );
            }
            else
            {
                LegacyTemplate legacy = new LegacyTemplate();
                javaCompileStatus = legacy.compile( xml_source, class_dir,
                    class_name, class_path );
            }
        }
        
        return javaCompileStatus;
    }
   
    private void compileElement( Element element )
    throws ServletException, IOException
    {
	String tag_name = element.getTagName();
	String class_att;
	
	if ( "template".equals( tag_name ) )
	{
	    String facilityclass = element.getAttribute( "facilityclass" );
	    if ( facilityclass == null || facilityclass.length() == 0 )
            {
                createTemplateVariable( "facility", Facility.class );
		writer.println( "facility=request.getFacility();" );
            }
	    else
	    {
                Class c=null;
                try
                {
                    c = Class.forName( facilityclass );
                }
                catch ( ClassNotFoundException e )
                {
                    throw new ServletException( "Class not found: " + facilityclass );
                }
                createTemplateVariable( "facility", c );
		writer.print( "facility=(" );
		writer.print( facilityclass );
		writer.println( ")request.getFacility();" );
	    }
	    compileChildren( element, false, false );
	}
	else if ( "preparation".equals( tag_name ) )
	{
	    compileChildren( element, false, false );
	    preparation_done = true;
	}
	else if ( "declare".equals( tag_name ) )
	{
	    compileDeclare( element );
	}
	else if ( "building".equals( tag_name) )
	{
	    compileBuilding ( element );
	}
	else if ( "assign".equals( tag_name ) )
	{
	    compileAssign( element );
	}
	else if ( "call".equals( tag_name ) )
	{
	    compileCall( element );
	    writer.println( ";" );
	}
	else if ( "if".equals( tag_name ) )
	{
	    compileIf( element );
	}
	else if ( "htmlfragment".equals( tag_name ) )
	{
	    compileChildren( element, false, true );
	}
	else if ( "textarea".equals( tag_name ) )
	{
	    compileTextarea( element );
	}
	else if ( "filter".equals( tag_name ) )
	{
	    compileFilter( element );
	}
	else
	{
	    if ( "html".equals( tag_name ) )
	    {
		preparation_done = true;
                writer.println( "           response.setContentType(\"text/html; charset=utf-8\");" );
		writer.println( "           writer = response.getWriter();" );
                // declare as loose HTML4 - this is an attempt to get I.E. 6 in standards mode
                writer.println( "           writer.println( \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0 Transitional//EN\\\" " +
                                "\\\"http://www.w3.org/TR/html4/loose.dtd\\\">\" );" );
	    }
	    if ( "body".equals( tag_name ) )
	    {
		class_att = element.getAttribute( "class" );
		// make compile time record of class
		html_body_class = class_att;
		// make run time record of class
		if ( class_att!=null && class_att.length() > 0 )
		{
		    writer.print( "           response.setHtmlBodyClass( \"" );
		    writer.print( class_att );
		    writer.println( "\" );" );
		}
	    }
	    
	    compileChildren( element, true, true );
	}
    }
    
    
    /**
     * Compile a textarea tag. This needs special handling as normally
     * the textarea tag is collapsed by the XML parser from two tags
     * to one and HTML needs two.
     * @param element The textarea element from the XML parsing.
     */
    private void compileTextarea(Element element) throws ServletException, IOException
    {
        writer.print("writer.print(\"<textarea ");
        writeAttributes(element);
        writer.print(">\");");
        compileChildren(element, false, true);
        writer.print("writer.print(\"</textarea>\");");
        
    }

    /**
     * Support for the legacy templates building tag.
     * @param element The node element.
     */
    private void compileBuilding(Element element)
    {
        NamedNodeMap attributes = element.getAttributes();
        writer.write("insertInteractive( request, writer, arrayToMap(new String[][]{");
        for (int attributeNo = 0; attributeNo < attributes.getLength(); attributeNo++)
        {
            if (attributeNo > 0)
            {
                writer.write(",");
            }
            Node attribute = attributes.item(attributeNo);
            writer.write( "{ \""+ attribute.getNodeName()+ "\" , \""+ attribute.getNodeValue()+ "\"}\n");
            
        }
        writer.write("}));\n");
    }

    private void compileChildren( Element element, boolean output_parent, boolean output_children )
    throws ServletException, IOException
    {
	int i, j, k;
	Node child_node;
	Element child_element;
	Attr child_attribute;
	CharacterData child_chars;
	boolean start_tag_open=false;
	boolean element_empty=true;
	
	if ( output_parent )
	{
	    start_tag_open = true;
	    writer.print( "           writer.print( \"<" );
	    writer.print( element.getTagName() );
	    
	    writeAttributes(element);
	}
	
	
	NodeList nodelist = element.getChildNodes();
	for ( i=0; i< nodelist.getLength(); i++ )
	{
	    child_node = nodelist.item( i );
	    if ( child_node.getNodeType() == Node.ELEMENT_NODE )
	    {
		if ( start_tag_open )
		{
		    writer.println( ">\" );" );
		    start_tag_open = false;
		}
		element_empty = false;
		
		child_element = (Element)child_node;
		compileElement( child_element );
	    }
	    if ( output_children && preparation_done &&
	    (child_node.getNodeType() == Node.CDATA_SECTION_NODE ||
	    child_node.getNodeType() == Node.TEXT_NODE )	    )
	    {
		if ( start_tag_open )
		{
		    writer.println( ">\" );" );
		    start_tag_open = false;
		}
		element_empty = false;
		
		child_chars = (CharacterData)child_node;
		int l = child_chars.getLength();
		String chunk;
		String hex;
		if ( l > 0 )
		    for ( j=0; j<l; j+=100 )
		    {
			chunk = child_chars.substringData( j, 100 );
			writer.print( "         writer.print( " );
			compileStringLiteral( chunk );
			writer.println( " );" );
		    }
	    }
	}
	
	if ( output_parent )
	{
	    if ( element_empty )
		writer.println( " />\" );" );
	    else
	    {
		writer.println( "           writer.print( \"</" + element.getTagName() + ">\" );" );
	    }
	}
    }
    
    
    
    /**
     * Writes through all the attributes of this element.
     * @param element
     */
    private void writeAttributes(Element element)
    {
        int i;
        Node child_node;
        Attr child_attribute;
        NamedNodeMap nodemap = element.getAttributes();
	    for ( i=0; i< nodemap.getLength(); i++ )
	    {
		child_node = nodemap.item( i );
		if ( child_node.getNodeType() == Node.ATTRIBUTE_NODE )
		{
		    child_attribute = (Attr) child_node;
		    writer.print( " " );
		    writer.print( child_attribute.getName() );
		    writer.print( "=\\\"" );
		    writer.print( child_attribute.getValue() );
		    writer.print( "\\\"" );
		}
	    }
    }

    private void compileDeclare( Element element )
    throws ServletException, IOException
    {
	String type = element.getAttribute( "type" );
	String name = element.getAttribute( "name" );
	
	if ( "String".equals( type ) )
	{
	    type = "java.lang.String";
	}
	
	Class c = getTemplateVariableClass( name );
	if ( c!=null )
	    throw new ServletException( "Can't declare same variable twice in one template." );
	
	try
	{
	    if ( "boolean".equals( type ) )
	    {
		createTemplateVariable( name, Boolean.TYPE );
		return;
	    }
	    
	    if ( "java.lang.String".equals( type ) )
	    {
		createTemplateVariable( name, String.class );
		return;
	    }
	}
	catch ( Exception e )
	{
	    throw new ServletException( "Template bytecode compilation error: invalid declaration." );
	}
	
	throw new ServletException( "Declared variables in templates must be boolean or String." );
    }
    
    
    private CharacterData getCharacterData( Element parent )
    throws ServletException, IOException
    {
	Node child_node;
	NodeList nodelist = parent.getChildNodes();
	for ( int i=0; i< nodelist.getLength(); i++ )
	{
	    child_node = nodelist.item( i );
	    if ( child_node instanceof CharacterData )
	    {
		return (CharacterData)child_node;
	    }
	}
	return null;
    }
    
    
    private Element[] getNamedElements( Element parent, String[] tag_names )
    throws ServletException, IOException
    {
	int i, j, pass, count;
	Node child_node;
	Element child_element;
	NodeList nodelist = parent.getChildNodes();
	Element[] expressions=null;
	
	for ( pass=0; pass<2; pass++ )
	{
	    count=0;
	    for ( i=0; i< nodelist.getLength(); i++ )
	    {
		child_node = nodelist.item( i );
		if ( child_node.getNodeType() == Node.ELEMENT_NODE )
		{
		    child_element = (Element)child_node;
		    for ( j=0; j < tag_names.length; j++ )
		    {
			if ( tag_names[j].equals( child_element.getTagName() ) )
			{
			    if ( pass==0 )
				count++;
			    else
				expressions[count++] = child_element;
			    break;
			}
		    }
		    
		}
	    }
	    if ( expressions == null )
		expressions = new Element[count];
	}
	
	return expressions;
    }
    
    private Class compileExpression( Element element )
    throws ServletException, IOException
    {
	if ( "literal".equals( element.getTagName() ) )
	    return compileLiteral( element );
	else if ( "variable".equals( element.getTagName() ) )
	    return compileVariable( element );
	else if ( "assign".equals( element.getTagName() ) )
	    return compileAssign( element );
	else if ( "call".equals( element.getTagName() ) )
	    return compileCall( element );
	else if ( "class".equals( element.getTagName() ) )
	    return compileClass (element );
	
	throw new ServletException( "Template bytecode compilation error: unknown expression." );
    }
    
    private void compileStringLiteral( String literal )
    throws IOException
    {
	int l, i, k;
	String hex;
	
	l = literal.length();
	writer.print( "\"" );
	for ( k=0; k<l; k++ )
	{
	    if ( literal.charAt( k ) == '\n' )
	    {
		writer.print( "\\n" );
	    }
	    else if ( literal.charAt( k ) == '\r' )
	    {
		writer.print( "\\r" );
	    }
	    else if ( literal.charAt( k ) == '\\' )
	    {
		writer.print( "\\\\" );
	    }
	    else if ( literal.charAt( k ) == '"' )
	    {
		writer.print( "\\\"" );
	    }
	    else if ( literal.charAt( k ) < ' ' || literal.charAt( k ) > '~' )
	    {
		hex = Integer.toHexString( (int)literal.charAt( k ) );
		writer.print( "\\u" );
		for ( i=hex.length(); i<4; i++ )
		    writer.print( "0" );
		writer.print( hex );
	    }
	    else
	    {
		writer.print( literal.charAt( k ) );
	    }
	}
	writer.print( "\"" );
    }
    
    private Class compileClass( Element element)
    throws ServletException
    {
    	String name = element.getAttribute("name");
    	try
	{
    		writer.print(name);
    		return Class.forName(name);
	}
    	catch (ClassNotFoundException cnfe)
	{
    	    throw new ServletException( "Could not find class: "+ name );
    	}
    }
    
    private Class compileLiteral( Element element )
    throws ServletException, IOException
    {
	String type = element.getAttribute( "type" );
	String strcdata=null;
	CharacterData cdata = getCharacterData( element );
	if ( cdata != null )
	    strcdata = cdata.getData();
	else
	    strcdata = "";
	
	if ( "String".equals( type ) )
	    type = "java.lang.String";
	
	if ( "boolean".equals( type ) )
	{
	    if ( strcdata!=null &&
	    strcdata.length()==4 &&
	    strcdata.toLowerCase().equals( "true" ) )
		writer.print( "true" );
	    else
		writer.print( "false" );
	    return Boolean.TYPE;
	}
	else if ( "java.lang.String".equals( type ) )
	{
	    compileStringLiteral( strcdata );
	    return java.lang.String.class;
	}
	else
	{
	    throw new ServletException( "Literals can only be of type boolean or String." );
	}
    }
    
    private Class compileVariable( Element element )
    throws ServletException, IOException
    {
	try
	{
	    String name = element.getAttribute( "name" );
	    Class vclass;
	    synchronized ( template_variable_class_table )
	    {
		vclass = getTemplateVariableClass( name );
		if ( vclass == null )
		    throw new ServletException( "Unknown variable." );
		writer.print( " " );
		writer.print( name );
		writer.print( " " );
	    }
	    return vclass;
	}
	catch ( Exception e )
	{
	    //request.facility.logException( writer, "XmlTemplateSession", "compileVariable",
	    //"Technical error trying to compile template.",
	    //e );
	    throw new ServletException( "Template bytecode compilation error: invalid variable." );
	}
    }
    
    private Class compileAssign( Element element )
    throws ServletException, IOException
    {
	String name = element.getAttribute( "name" );
	
	Class left_class = getTemplateVariableClass( name );
	if ( left_class==null )
	    throw new ServletException( "Can't make assignment to undeclared variable." );
	
	Element[] expressions = getNamedElements( element, expression_tag_names );
	if ( expressions.length != 1 )
	    throw new ServletException( "Expected exactly 1 expression in assignment." );
	
	writer.print( "         " );
	writer.print( name );
	writer.print( " = " );
	Class right_class = compileExpression( expressions[0] );
	writer.println( ";" );
	
	if ( !left_class.isAssignableFrom( right_class ) )
	    throw new ServletException( "Type mismatch in assignment." );
	
	return left_class;
    }
    
    
    private Class compileCall( Element element )
    throws ServletException, IOException
    {
	try
	{
	    Element[] parts = getNamedElements(  element, call_tag_names  );
	    if ( parts == null || parts.length != 2 )
		throw new ServletException( "Call element in template must have target and parameters." );
	    
	    if ( !"target".equals( parts[0].getTagName() ) ||
	    !"parameters".equals( parts[1].getTagName() ) )
		throw new ServletException( "Call element in template must have target and parameters." );
	    
	    Element[] target_expression = getNamedElements(  parts[0], expression_tag_names  );
	    if ( target_expression == null || target_expression.length != 1 )
		throw new ServletException( "Expected single expression in target element." );
	    
	    Class targetclass = compileExpression( target_expression[0] );
	    writer.print( "." );
	    String method_name = parts[0].getAttribute( "method" );
	    writer.print( method_name );
	    
	    Element[] expressions = getNamedElements(  parts[1], expression_tag_names  );
	    
	    Class[] param_classes = new Class[expressions.length];
	    
	    writer.print( "(" );
	    for ( int i=0; i<expressions.length; i++ )
	    {
		if ( i>0 )
		    writer.print( "," );
		writer.print( " " );
		param_classes[i] = compileExpression( expressions[i] );
	    }
	    writer.print( " )" );
	    
	    
	    // Method method = fclass.getMethod( method_name, param_classes );
	    // can;t just call getMethod because that requires exact match on
	    // the parameters but the parameters here might be subclasses or
	    // implementations of the declared parameters of the target method
	    
	    // only list the public methods
	    Method target_method=null;
	    Method[] methods = targetclass.getMethods();
	    Class[] target_param_classes;
	    int m, p, n;
	    n=0;
	    int[] scores = new int[methods.length];
	    int best=-1;
	    Class superclass;
	    for ( m=0; m<methods.length; m++ )
	    {
		scores[m]=-1;
		if ( !method_name.equals( methods[m].getName() ) )
		    continue;  // not the right method name - keep looking
		target_param_classes = methods[m].getParameterTypes();
		if ( target_param_classes.length != param_classes.length )
		    continue; // not the right no. of params - keep looking
		
		for ( p=0; p<param_classes.length; p++ )
		{
		    if ( !target_param_classes[p].isAssignableFrom( param_classes[p] ) )
			break;
		}
		if ( p<param_classes.length )
		    continue;  // not the right types of params - keep looking
		
		// this method matches name and parameters
		if ( best==-1 )
		    best = m;
		scores[m]=0;
		for ( p=0; p<param_classes.length; p++ )
		{
		    for ( superclass = param_classes[p]; superclass != null ; superclass = superclass.getSuperclass() )
		    {
			if ( target_param_classes[p].equals( superclass ) )
			    break;
			scores[m]++;
		    }
		    if ( scores[m] < scores[best] )
			best = m;
		}
		n++;
	    }
	    
		
	    if ( best == -1 )
		throw new ServletException( "Unknown method called in template: "+ method_name + "()" );
	    
	    target_method = methods[best];
	    
	    return target_method.getReturnType();
	}
	catch ( RuntimeException runex )
	{
	    throw new ServletException( "Technical error trying to insert iteractive item." );
	}
    }
    
    private void compileIf( Element element )
    throws ServletException, IOException
    {
	int i;
	
	
	Node child_node;
	Element child_element;
	NodeList nodelist = element.getChildNodes();
	for ( i=0; i< nodelist.getLength(); i++ )
	{
	    child_node = nodelist.item( i );
	    if ( child_node.getNodeType() == Node.ELEMENT_NODE )
	    {
		child_element = (Element)child_node;
		if ( "test".equals( child_element.getTagName() ) )
		{
		    Element[] expressions = getNamedElements(  child_element, expression_tag_names  );
		    if ( expressions==null || expressions.length != 1 )
			throw new ServletException( "Expected exactly 1 expression in test clause of if statement." );
		    
		    writer.print( "         if ( " );
		    Class exclass = compileExpression( expressions[0] );
		    if ( !exclass.equals( Boolean.TYPE ) )
			throw new ServletException( "Expected boolean expression in test clause of if statement." );
		    writer.println( " )" );
		}
		else if ( "then".equals( child_element.getTagName() ) )
		{
		    writer.println( "         {" );
		    compileChildren( child_element, false, true );
		    writer.println( "         }" );
		}
		else if ( "else".equals( child_element.getTagName() ) )
		{
		    writer.println( "         else" );
		    writer.println( "         {" );
		    compileChildren( child_element, false, true );
		    writer.println( "         }" );
		}
	    }
	}
	
    }
    
    
    private void compileFilter( Element element )
    throws ServletException, IOException
    {
	Element[] elements = getNamedElements( element, htmlfragment_tag_names );
	if ( elements.length != 1 )
	    throw new ServletException( "Expected one htmlfragment in filter element." );
	compileChildren( elements[0], false, true );
	
    }
    
    
    public void createTemplateVariable( String name, Class class_ref )
    {
	synchronized ( template_variable_class_table )
	{
	    if ( class_ref.equals( Void.TYPE ) )
		throw new IllegalArgumentException( "Template variable cannot be of type void." );
	    
	    template_variable_class_table.put( name, class_ref );
	    writer.print( "         " );
	    String cname = class_ref.toString();
	    if ( cname.indexOf( ' ' ) >=0 )
		cname = cname.substring( cname.indexOf( ' ' )+1 );
	    writer.print( cname );
	    writer.print( " " );
	    writer.print( name );
	    writer.println( ";" );
	}
    }
    
    public Class getTemplateVariableClass( String name )
    {
	synchronized ( template_variable_class_table )
	{
	    return (Class)template_variable_class_table.get( name );
	}
    }
    
    
    private XmlTemplateClassLoader getSuitableClassLoader()
    {
	synchronized ( class_loaders )
	{
	    String dirname = class_dir.getAbsolutePath();
	    XmlTemplateClassLoader class_loader =
	    (XmlTemplateClassLoader)class_loaders.get( dirname );
	    if ( class_loader != null )
		return class_loader;
	    
	    // a special class loader is used to load compiled templates because they
	    // a saved to a specific directory but it needs access to the super 
	    // classes of the templates which ought to be loadable by the same class
	    // loader that loaded this class
	    class_loader = new XmlTemplateClassLoader( class_dir, this.getClass().getClassLoader() );
	    class_loaders.put( dirname, class_loader );
	    return class_loader;
	}
    }
    
    
    private static final byte[][] xml_heads =
    {
	{ '<', '?', 'x', 'm', 'l' },
	{ -2, -1, 0, '<', 0, '?', 0, 'x', 0, 'm', 0, 'l' },
	{ -1, -2, '<', 0, '?', 0, 'x', 0, 'm', 0, 'l', 0 }
    };
    
    private boolean isXml()
    throws IOException
    {
	InputStream input=null;
	try
	{
	    input = new FileInputStream( xml_source );
	    int i, j;
	    byte[] head = new byte[12];
	    input.read( head );
	    for ( j=0; j<xml_heads.length; j++ )
	    {
		for ( i=0; i<xml_heads[j].length; i++ )
		    if ( head[i] != xml_heads[j][i] )
			break;
		if ( i == xml_heads[j].length )
		    return true;
	    }
	    return false;
	}
	finally
	{
	    try
	    {
		if ( input!=null )
		    input.close();
	    }
	    catch ( Exception e )
	    {
	    }
	}
    }
    
    
    
    
}
