/*
 *  Copyright 2002 Phillip Lord, Victoria University of Manchester
 *
 *  This file is part of myGrid.  Further information, and the
 *  latest version, can be found at http://www.mygrid.info
 *
 *  myGrid is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation; either version 2.1
 *  of the License, or (at your option) any later version.
 *
 *  myGrid is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with myGrid; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package uk.ac.man.cs.img.xml.merge;
import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.StringTokenizer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import org.w3c.dom.Element;

/**
 * AntMerge.java
 *
 * Merge two Ant build files. This merge allows specification of a
 * "superclass" ant build file, and another which over-rides a few
 * specific pieces of it.
 *
 * Created: Wed Nov 27 22:23:06 2002
 *
 * @author Phillip Lord
 * @version $Id: AntMerge.java,v 1.14 2003/09/24 12:24:06 lordp Exp $
 */

public class AntMerge
{
    private final static String defaultProgramName = "antmerge";
    private final static String defaultMergeFile= "build-in.xml";
    private final static String defaultOutputFile = "build.xml";
    private final static String emptyFile = "empty.xml";
    private final static String defaultMainFile = "default.xml";

    private String programName;
    private String defaultInstallationDirectory;
    private String defaultUserDirectory;

    // some flags
    private boolean debug = false;
    private boolean mergeFileEmpty = false;
    private boolean suppressAntmergeXML = false;
    

    // set up DOM parsers and generators
    private DocumentBuilderFactory builderFactory;

    private DocumentBuilder builder;


    /**
     * This constructor is for the command line launch
     */
    protected AntMerge( String[] args ) throws ParserConfigurationException
    {
        // Parse some options that are passed in from the launch
        // script, rather than the command line per se, although, they
        // can be over ridden from the command line.
        String installationDirectory = defaultInstallationDirectory =
            (System.getProperty( "installation.directory" ) == null) ?
            "." : System.getProperty( "installation.directory" );
        String userDirectory = defaultUserDirectory =
            (System.getProperty( "user.extra.directory" ) == null) ?
            "." : System.getProperty( "user.extra.directory" );
        this.programName =
            (System.getProperty( "program.name" ) == null) ?
            defaultProgramName : System.getProperty( "program.name" );

        // set up some parsers for later use
        builderFactory = new org.apache.xerces.jaxp.DocumentBuilderFactoryImpl();
        builder = builderFactory.newDocumentBuilder();

        // The various objects that we want to set

        // The main file that we are going to start with. This is
        // something like the super class
        String mainFile = defaultMainFile;
        // The merge file that we are going to merge with. This is
        // something like the sub class.  TODO: want to generalise
        // this somewhat. Something like, the command line option if
        // it exists build-in.xml if it exists, NAME-in.xml, where
        // NAME is anything, if it exists, and use NAME as the project
        // name.
        String mergeFile = defaultMergeFile;
        // The output file that we are going to output to.
        String outputFile = defaultOutputFile;
        
        LongOpt[] options = new LongOpt[ 10 ];

        int c;
        String arg;

        options[ 0 ] = new LongOpt( "help", LongOpt.NO_ARGUMENT, null, 'h' );
        options[ 1 ] = new LongOpt( "version", LongOpt.NO_ARGUMENT, null, 'v' );
        options[ 2 ] = new LongOpt( "installation_directory", LongOpt.REQUIRED_ARGUMENT, null, 'i' );
        options[ 3 ] = new LongOpt( "user_directory", LongOpt.REQUIRED_ARGUMENT, null, 'u' );
        options[ 4 ] = new LongOpt( "output", LongOpt.REQUIRED_ARGUMENT, null, 'o' );
        options[ 5 ] = new LongOpt( "mainfiles", LongOpt.REQUIRED_ARGUMENT, null, 'm' );
        options[ 6 ] = new LongOpt( "mergefile", LongOpt.REQUIRED_ARGUMENT, null, 'n' );
        options[ 7 ] = new LongOpt( "clean", LongOpt.NO_ARGUMENT, null, 'c' );
        options[ 8 ] = new LongOpt( "debug", LongOpt.NO_ARGUMENT, null, 'd' );
        options[ 9 ] = new LongOpt( "no_antmerge", LongOpt.NO_ARGUMENT, null, 's' );
        
        Getopt g = new Getopt( programName, args, "cdhvi:u:o:m:n:s", options );

        while ( (c = g.getopt()) != -1 ){

            switch (c)
                {

                case 'h':
                case 0:
                    help();
                    System.exit( 0 );
                    break;

                case 'v':
                case 1:
                    version();
                    System.exit( 0 );
                    break;

                case 'i':
                case 2:
                    installationDirectory = g.getOptarg();
                    break;

                case 'u':
                case 3:
                    userDirectory = g.getOptarg();
                    break;

                case 'o':
                case 4:
                    outputFile = g.getOptarg();
                    break;

                case 'm':
                case 5:
                    mainFile = g.getOptarg();
                    break;

                case 'n':
                case 6:
                    mergeFile = g.getOptarg();
                    break;

                case 'c':
                case 7:
                    mergeFileEmpty = true;
                    break;

                case 'd':
                case 8:
                    debug = true;
                    break;
                    
                    
                case 's':
                case  9:
                    suppressAntmergeXML = true;
                    break;

                }

        }

        if( mainFile == defaultMainFile ){
            if( args.length > g.getOptind() ){
                mainFile = args[ g.getOptind() ];
            }
        }


        try{
            setInstallationDirectory( installationDirectory );
            setUserDirectory( userDirectory );

            // use the empty file if we are told to
            if( mergeFileEmpty ){
                setMergeFile( new File( buildFilesDirectory, emptyFile ) );
            }
            else{
                setMergeFile( mergeFile );
            }

            // force antmerge onto the front of the main files....
            if( !suppressAntmergeXML ){
                mainFile = "antmerge," + mainFile;
            }


            setMainFile( mainFile );
            setOutputFile( outputFile );
        }
        catch( FileNotFoundException fnfe ){
            System.out.println( programName + ": " + fnfe.getMessage() );
            System.out.println( "Try " + programName + " -h for usage" );
            System.exit( -1 );
        }

        try{
            mergeFiles();
        }
        catch( IOException io ){
            System.out.println( io.getMessage() );
            System.exit( -1 );
        }
        catch( ParserConfigurationException pce ){
            System.out.println( "There is a serious error. The application is probably broken" );
            System.out.println( pce.getMessage() );
            System.exit( -2 );
        }
        catch( SAXException se ){
            System.out.println( se.getMessage() );
            System.exit( -3 );
        }

        System.out.println( "Generated file " + this.outputFile.getName() );
    }

    private void debug( String message )
    {
        if( debug ){
            System.out.println( "Debug: " +  message );
        }
    }

    public void help()
    {

        System.out.println( "Usage: " + programName + "[OPTION]..." );
        System.out.println( "  or:  " + programName + "[OPTION]... [MAIN_FILE]..." );
        System.out.println( "Merge the ant build-file MAIN_FILE with the another." );
        System.out.println( "The merging process provides something like inheritance for ant build files" );
        System.out.println( "Unless explicitly set a FILE with the name build-in.xml is used as the merge file" );
        System.out.println( "A number of default MAIN_FILES are supplied, and additionally the directory " );
        System.out.println(  "\"" + defaultUserDirectory + "\" is searched." );

        System.out.println( "The OUTPUT_FILE defaults to " + defaultOutputFile + "." );
        System.out.println( "   -c,  --clean     output a clean build file without merging" );
        System.out.println( "   -h,  --help      output this message and exit" );
        System.out.println( "   -i,  --installation_directory   specify the installation directory of the application" );
        System.out.println( "   -m,  --mainfile  specify MAIN_FILE" );
        System.out.println( "   -n,  --mergefile specify FILE" );
        System.out.println( "   -o,  --output    specify OUTPUT_FILE" );
        System.out.println( "   -s,  --no_antmerge suppress addition of antmerge main file" );
        System.out.println( "   -v,  --version   display version information and exit" );

        System.out.println( );
        System.out.println( "The most recent version is available from <http://www.russet.org.uk>" );
    }

    public void version()
    {
        System.out.println( defaultProgramName + getVersion() );
        System.out.println( "Written by Phillip Lord (p.lord@russet.org.uk)." );
        System.out.println( );
        System.out.println( "Copyright (C) 2002 Phillip Lord, The Victoria University of Manchester" );
        System.out.println( "This is free software; see the source for copying conditions. There is NO" );
        System.out.println( "warranty: not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.");
    }


    private File fileExistsTest( String name )
    {
        return fileExistsTest
            ( System.getProperty( "user.dir" ), name );

    }


    private File fileExistsTest( String directory, String name )
    {
        return fileExistsTest( new File( directory ), name );
    }

    // does a file exist? If yes return the file, if no null
    private File fileExistsTest( File directory, String name )
    {
        File file = new File( directory, name );
        debug( "Testing for file existence " + file.getAbsolutePath() );
        if( file.exists() ){
            return file;
        }
        debug( "File not found" );
        return null;
    }


    private File installationDirectory;
    private File buildFilesDirectory;

    public void setInstallationDirectory( String installation )
    {
        setInstallationDirectory( new File( installation ) );
    }

    public void setInstallationDirectory( File installationDirectory )
    {
        debug( "Setting installation directory: "
               + installationDirectory.getAbsolutePath() );
        this.installationDirectory = installationDirectory;
        this.buildFilesDirectory =
            new File( installationDirectory, "etc" );
    }

    private File userDirectory;

    public void setUserDirectory( String user )
    {
        setUserDirectory( new File( user ) );
    }

    public void setUserDirectory( File userDirectory )
    {
        debug( "Setting user directory: " + userDirectory.getAbsolutePath() );
        this.userDirectory = userDirectory;
    }

    private String[] splitMainFiles( String mainFiles )
    {
        debug( "Spliting mainfile " + mainFiles + " into tokens" );
        StringTokenizer tokenizer = new StringTokenizer( mainFiles, "+," );
        String[] retn = new String[ tokenizer.countTokens() ];
        int i = 0;
        while( tokenizer.hasMoreTokens() ){
            retn[ i++ ] = tokenizer.nextToken();
        }
        return retn;
    }

    public void setMainFile( String mainFile )
        throws FileNotFoundException
    {
        debug( "Setting main file " + mainFile );
        setMainFiles( splitMainFiles( mainFile ) );
    }

    public void setMainFile( File mainFile )
    {
        debug( "Setting main file:" + mainFile.getAbsolutePath() );
        this.mainFiles = new File[ 1 ];
        mainFiles[ 0 ] = mainFile;
    }

    private File[] mainFiles;
    public void setMainFiles( String[] mainFilesString ) throws FileNotFoundException
    {
        mainFiles = new File[ mainFilesString.length ];

        for( int i = 0; i < mainFiles.length; i++ ){
            mainFiles[ i ] = resolveMainFile( mainFilesString[ i ] );
        }
    }


    public void setMainFiles( File[] mainFiles )
    {
        this.mainFiles = mainFiles;
    }

    protected File resolveMainFile( String file ) throws FileNotFoundException
    {
        File mainFile;

        if( !file.endsWith( ".xml" ) ){
            file = file + ".xml";
        }

        debug( "Resolve main file " + file );
        debug( "Searching for main file: " + file );
        debug( "Searching for main file: in cwd" );

        // does this file exist in the current working directory
        if( (mainFile = fileExistsTest( file )) != null ){
            return mainFile;
        }

        debug( "Searching for main file: in user directory" );

        // does it exist in the users directory
        if( (mainFile = fileExistsTest( userDirectory, file )) != null ){
            return mainFile;
        }

        debug( "Searching for main file: in installation build files directory" );

        // does it exist in the installation directory
        if( (mainFile = fileExistsTest( buildFilesDirectory, file )) != null ){
            return mainFile;
        }

        debug( "Main file not found" );

        // oh dear, it doesn't exist anywhere
        throw new FileNotFoundException
            ( "The main file " + file + " was not found" );
    }

    private File mergeFile;

    public void setMergeFile( String mergeFile ) throws FileNotFoundException
    {
        debug( "Setting merge file: " + mergeFile );
        File file;
        if( (file = fileExistsTest( mergeFile )) != null  ){
            setMergeFile( new File( mergeFile ) );
            return;
        }

        if( (file = fileExistsTest( mergeFile + ".xml" )) != null ){
            setMergeFile( new File( mergeFile ) );
            return;
        }

        throw new FileNotFoundException
            ( "The merge file " + mergeFile + " was not found" );
    }


    public void setMergeFile( File mergeFile )
    {
        debug( "Setting merge file " + mergeFile.getAbsolutePath() );
        this.mergeFile = mergeFile;
    }

    private File outputFile;

    public void setOutputFile( String file ) throws FileNotFoundException
    {
        setOutputFile( new File( file ) );
    }

    public void setOutputFile( File outputFile )
    {
        debug( "Setting output file " + outputFile );
        this.outputFile = outputFile;
    }


    public String getVersion()
    {
        return Version.VERSION;
    }


    public void mergeFiles()
        throws IOException, ParserConfigurationException, SAXException
    {
        debug( "Merging files" );
        Document output = merge();

        // Output the document
        OutputFormat    format  = new OutputFormat( output, null, true );
        StringWriter  stringOut = new StringWriter();
        XMLSerializer    serial = new XMLSerializer( stringOut, format );
        serial.asDOMSerializer();
        serial.serialize( output.getDocumentElement() );

        debug( "Output file generated:- \n" + stringOut.toString() );

        debug( "outputing to file " );

        FileWriter fileWriter = new FileWriter( outputFile );
        fileWriter.write( stringOut.toString() );
        fileWriter.close();
    }

    public Document merge()
        throws SAXException, IOException
    {
        debug( "Merging" );

        debug( "Parsing file " + mainFiles[0] );
        
        String mainFileNames = "";
        
        Document documentSoFar = builder.parse( mainFiles[ 0 ] );
        documentSoFar.getDocumentElement().insertBefore
            ( documentSoFar.createComment
              ( "Generated from " + mainFiles[ 0 ].getAbsolutePath() ),
              documentSoFar.getDocumentElement().getFirstChild() );

        
        
        
        for( int i = 0; i < mainFiles.length; i++ ){
            documentSoFar = merge( documentSoFar, mainFiles[ i ] );
           
            mainFileNames = mainFileNames + mainFiles[ i ] + ",";
        }
        documentSoFar = merge( documentSoFar, mergeFile );
        
        
        if( !suppressAntmergeXML ){
            // this section puts in the target which checks to see
            // whether the parental files are out of date.
            Element target = documentSoFar.createElement( "target" );
            target.setAttribute( "name", "antmerge.test.main" );
            Element condition = documentSoFar.createElement( "condition" );
            condition.setAttribute( "property", "build.main.required" );
            target.appendChild( condition );
            Element orCondition = documentSoFar.createElement( "or" );
            condition.appendChild( orCondition );
            for( int i = 0; i < mainFiles.length; i++ ){
                orCondition.appendChild
                    ( generateUpdateTo( documentSoFar, mainFiles[ i ].getAbsolutePath() ) );
            }
            documentSoFar.getDocumentElement().insertBefore
                ( target, documentSoFar.getDocumentElement().getFirstChild() );
        }
        

        documentSoFar.getDocumentElement().insertBefore
            ( documentSoFar.createComment
              ( "This file has been auto-generated by the AntMerge tool. Do not edit" ),
              documentSoFar.getDocumentElement().getFirstChild() );

        
        return documentSoFar;
    }

    private Element generateUpdateTo( Document document, String mainFile )
    {
        Element not = document.createElement( "not" );
        Element uptodate = document.createElement( "uptodate" );
        uptodate.setAttribute( "srcfile", mainFile );
        uptodate.setAttribute( "targetfile", "build.xml" );
        
        not.appendChild( uptodate );
        return not;        
    }
    
    protected Document merge( Document mainDocument, File mergeFile )
        throws SAXException, IOException
    {
        debug( "Parsing file " + mergeFile );
        Document documentMerge = builder.parse( mergeFile );

        Document output = merge( mainDocument, documentMerge );
        output.getDocumentElement().insertBefore
            ( output.createComment
              ( "Generated from " + mergeFile.getAbsolutePath() ),
              output.getDocumentElement().getFirstChild() );
        return output;
    }


    protected Document merge( Document mainDocument, Document mergeDocument )
    {
        return merge( mainDocument, mergeDocument, builder.newDocument() );
    }

    // These are the unique predicates for the AntMerge.
    private static final UniquePredicate[] predicate = {
        // If an element has an ID attribute, then use this.
        UniqueIDAtttributePredicate.prototypeInstance(),
        // A target element can be identified uniquely by its name.
        new SuperTagUniqueAttributeTagPredicate( "target", "name" ),
        // A property can be uniquely identified by its name.
        new SuperVarUniqueAttributeTagPredicate( "property", "name", "value" ),
        // A property can be uniquely identified by its environment.
        new UniqueAttributeTagPredicate( "property", "environment" ),
        // A property for dealing with task defs
        new UniqueAttributeTagPredicate( "taskdef", "name" )
    };

    public Document merge( Document mainDocument, Document mergeDocument,
                           Document output )
    {
        // Merge the documents.
        MergeDocuments merge = new MergeDocuments( predicate );
        merge.merge( mainDocument, mergeDocument, output );

        // The project name, and default always comes from the merge
        // document.  We can't do this with the UniquePredicates
        // because even though we are using the attributes from the
        // merge document, we still need to carry on recursing down
        // the document.
        String projectName = mergeDocument.getDocumentElement()
            .getAttribute( "name" );
        String projectDefault = mergeDocument.getDocumentElement()
            .getAttribute( "default" );
        output.getDocumentElement().setAttribute( "name", projectName );
        output.getDocumentElement().setAttribute
            ( "default", projectDefault );

        return output;
    }

    public static void main(String[] args) throws Throwable
    {
        AntMerge antMerge = new AntMerge( args );
    }
} // MergeTest



/*
 * ChangeLog
 * $Log: AntMerge.java,v $
 * Revision 1.14  2003/09/24 12:24:06  lordp
 * SAX errors now cause non zero return which will halt the build.
 *
 * Revision 1.13  2003/09/15 11:32:18  lordp
 * Better version number support
 *
 * Revision 1.12  2003/09/08 11:02:10  lordp
 * Specials support.
 * Changed default main file
 *
 * Revision 1.11  2003/08/19 17:28:54  lordp
 * Removed early release warning
 *
 * Revision 1.10  2003/06/20 13:08:32  lordp
 * Automatically add antmerge as main file (unless forcibly suppressed).
 * Add autocoded references to main file for out of date checks
 *
 * Revision 1.9  2003/05/23 19:13:25  lordp
 * Removed tabs
 *
 * Revision 1.8  2003/05/19 14:46:58  alpdemirn
 * now copes with property environment
 *
 * Revision 1.7  2003/02/17 09:17:42  sharmann
 * 1. support builds on Windows platforms
 * 2. support multiple main files
 * 3. remove obsolete sources
 *
 * Revision 1.6  2002/12/20 15:41:04  lordp
 * Added support for multiple main files, and command line options to
 * support it
 *
 * Revision 1.5  2002/12/20 13:37:21  lordp
 * Small fixes
 *
 * Revision 1.4  2002/12/10 14:30:59  lordp
 * Added boilerplate
 *
 * Revision 1.3  2002/12/10 13:49:45  lordp
 * Improved merge file handling
 *
 * Revision 1.2  2002/12/05 18:12:28  lordp
 * Was searching for merge file rather than main file.
 * Changed some user output messages
 *
 * Revision 1.1  2002/12/04 17:19:51  lordp
 * Initial checkin
 *
 */


