/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 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 name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. 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.
 *
 * Copyright 2004 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: CastorSourceGenTask.java 6410 2006-11-17 12:06:24Z wguttmn $
 */
package org.castor.anttask;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
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.castor.xml.BackwardCompatibilityContext;
import org.castor.xml.InternalContext;
import org.exolab.castor.builder.SourceGenerator;
import org.exolab.castor.builder.binding.ExtendedBinding;
import org.exolab.castor.builder.factory.FieldInfoFactory;
import org.exolab.castor.xml.XMLException;
import org.exolab.castor.xml.schema.Schema;
import org.exolab.castor.xml.schema.SchemaContext;
import org.exolab.castor.xml.schema.SchemaContextImpl;
import org.exolab.castor.xml.schema.reader.Sax2ComponentReader;
import org.exolab.castor.xml.schema.reader.SchemaUnmarshaller;
import org.exolab.javasource.JClass;
import org.xml.sax.InputSource;
import org.xml.sax.Parser;
import org.xml.sax.SAXException;

/**
 * An <a href="http://ant.apache.org/">Ant</a> task to call the Castor
 * Source Generator. It can be passed a file, a directory, a Fileset or all
 * three.
 *
 * @author <a href="mailto:joel.farquhar@montage-dmc.com">Joel Farquhar</a>
 * @author <a href="mailto:ferret AT frii DOT com">Bruce Snyder</a>
 * @author <a href="mailto:wguttmn AT codehaus DOT org">Werner Guttmann</a>
 * @version $Revision: 6543 $ $Date: 2005-03-05 06:42:06 -0700 (Sat, 05 Mar 2005) $
 */
public final class CastorCodeGenTask extends MatchingTask {

    /** Message indicating that the method setJdoDescriptorCreation cannot be invoked. */
    private static final String PROBLEM_SETTING_JDO_DESC = 
        "Problem calling SourceGenerator.setJdoDescriptorCreation: ";

    /** Msg indicating class descriptor generation has been disabled. */
    private static final String DISABLE_DESCRIPTORS_MSG =
        "Disabling generation of Class descriptors";
    
    /** Msg indicating marshaling framework code will not be generated. */
    private static final String DISABLE_MARSHAL_MSG =
        "Disabling generation of Marshaling framework methods (marshal, unmarshal, validate).";
    
    /** Msg indicating that castor-testable code will be generated. */
    private static final String CASTOR_TESTABLE_MSG =
        "The generated classes will implement org.exolab.castor.tests.CastorTestable";
    
    /** Error message -- invalid line seperator. */
    private static final String INVALID_LINESEP_MSG =
        "Invalid value for lineseparator, must be win, unix, or mac.";
    
    /** Error message -- no schemas to run code generator on. */
    private static final String NO_SCHEMA_MSG =
        "At least one of the file, url or dir attributes, or a fileset element, must be set.";

    /** Castor XML context - the mother of all. */
    private InternalContext _internalContext;
    
    /** If processing one schema file, this lists the file. */
    private File _schemaFile = null;
    
    /** If processing one schema file from an URL, this lists the file. */
    private String _schemaURL = null;
    
    /** If processing all schemas in a directory, this lists the directory. */
    private File _schemaDir = null;
    
    /** If processing a fileset, this lists the fileset. */
    private Vector < FileSet > _schemaFilesets = new Vector < FileSet > ();

    // Begin Source Generator parameters
    /** The package that generated code will belong to. */
    private String _srcpackage;
    
    /** The directory that code will be generated into. */
    private String _todir;

    /** The directory that resources will be generated into, if specified. */
    private String _resourcesDirectory;

    /** Binding file for the code generator. */
    private String _bindingfile;
    
    /** Information about how to generate collections. */
    private String _types;
    
    /** Line seperator to use for generated code. */
    private String _lineseparator;
    
    /** If true, the code generator will be verbose. */
    private boolean _verbose;
    
    /** If true, non-fatal warnings will be suppressed.  Also, existing source
     *  files will be silently overwritten. */
    private boolean _warnings = true;
    
    /** If true, class descriptors will not be generated. */
    private boolean _nodesc;
    
    /** If true, marshaling code will not be generated. */
    private boolean _nomarshal;
    
    /**
     * If true, a mapping file will be generated (additionally).
     */
    private boolean _generateMapping;
    
    /** If true, Castor's CTF testable framework code will be generated. */
    private boolean _testable;
    
    /** Whether to generate code for imported schemas, too. */
    private boolean _generateImportedSchemas;
    
    /** Whether to generate SAX-1 compliant code. */
    private boolean _sax1;
    
    /** Whether enumerated type lookup should be performed in a case insensitive manner. */
    private boolean _caseInsensitive;
    
    /** CastorBuilderProperties file. */
    private String _properties;
    
    /** The name conflict strategy to use. */
    private String _nameConflictStrategy = "warnViaConsoleDialog";
    
    /** Name of the automatic clas name conflict strategy to use. */
    private String _automaticConflictStrategy = "xpath";

    /** Whether to generate JDO descriptors. */
    private boolean _generateJdoDescriptors;

    /**
     * Mode for printing JClass instances.
     */
    private String _jclassPrinterType = "standard";
    
    /** SourceGenerator instance. */
    private SourceGenerator _sgen;

    /**
     * No-arg constructor.
     */
    public CastorCodeGenTask() {
        super();
        _internalContext = new BackwardCompatibilityContext();
    }

    /**
     * Sets the individual schema that will have code generated for it.
     * 
     * @param file One schema file.
     */
    public void setFile(final File file) {
        _schemaFile = file;
    }

    /**
     * Sets an URL for one individual schema that will have code generated for it.
     * 
     * @param schemaURL URL for one schema file.
     */
    public void setSchemaURL(final String schemaURL) {
        _schemaURL = schemaURL;
    }


    /**
     * Sets the directory such that all schemas in this directory will have code
     * generated for them.
     * 
     * @param dir The directory containing schemas to process.
     */
    public void setDir(final File dir) {
        _schemaDir = dir;
    }

    /**
     * Adds a fileset to process that contains schemas to process.
     * 
     * @param set An individual file set containing schemas.
     */
    public void addFileset(final FileSet set) {
        _schemaFilesets.addElement(set);
    }

    /**
     * Sets the package that generated code will belong to.
     * 
     * @param pack The package that generated code will belong to.
     */
    public void setPackage(final String pack) {
        _srcpackage = pack;
    }

    /**
     * Sets the directory into which code will be generated.
     * 
     * @param dest The directory into which code will be generated.
     */
    public void setTodir(final String dest) {
        _todir = dest;
    }

    /**
     * Sets the directory into which resources will be generated. If not 
     * specified, resources will be generated in the same location as 
     * code.
     * 
     * @param dest The directory into which resources will be generated.
     */
    public void setResourcesDirectory(final String destination) {
        _resourcesDirectory = destination;
    }

    /**
     * Sets the binding file to be used for code generation.
     * 
     * @param bindingfile The binding file to be used for code generation.
     */
    public void setBindingfile(final String bindingfile) {
        _bindingfile = bindingfile;
    }

    /**
     * Sets the line seperator to use for code generation.
     * 
     * @param ls The line seperator to use for code generation.
     */
    public void setLineseparator(final String ls) {
        _lineseparator = ls;
    }

    /**
     * Sets the type factory for code generation.
     * 
     * @param tf The type factory to use for code generation.
     */
    public void setTypes(final String tf) {
        _types = (tf.equals("j2")) ? "arraylist" : tf;
    }

    /**
     * Sets whether or not code generation gives extra information about its work.
     * 
     * @param b If true, the code generator will be verbose.
     */
    public void setVerbose(final boolean b) {
        _verbose = b;
    }

    /**
     * Sets the name conflict strategy to use.
     * 
     * @param nameConflictStrategy The name conflict strategy to use
     */
    public void setNameConflictStrategy(final String nameConflictStrategy) {
        _nameConflictStrategy = nameConflictStrategy;
    }

    /**
     * Sets the name conflict strategy to use.
     * 
     * @param automaticConflictStrategy The automatic class name conflict strategy to use
     */
    public void setAutomaticConflictStrategy(final String automaticConflictStrategy) {
        _automaticConflictStrategy = automaticConflictStrategy;
    }

    
    /**
     * Sets whether or not non-fatal warnings should be suppressed.
     * 
     * @param b If true, non-fatal warnings will be suppressed. This additionally
     *        means that existing source files will be silently overwritten.
     */
    public void setWarnings(final boolean b) {
        _warnings = b;
    }

    /**
     * Sets whether or not class descriptors are generated.
     * 
     * @param b If true, class descriptors are generated.
     */
    public void setNodesc(final boolean b) {
        _nodesc = b;
    }

    /**
     * Sets whether or not marshaling methods are generated.
     * 
     * @param b If true, marshaling methods are generated.
     */
    public void setNomarshal(final boolean b) {
        _nomarshal = b;
    }

    /**
     * Sets whether CTF framework code is generated.
     * 
     * @param b If true, the generated code will be instrumented for the CTF.
     */
    public void setTestable(final boolean b) {
        _testable = b;
    }

    /**
     * Controls whether to generate code for imported schemas as well.
     * 
     * @param generateImportedSchemas True if code should be generated for imported schemas.
     */
    public void setGenerateImportedSchemas(final boolean generateImportedSchemas) {
        _generateImportedSchemas = generateImportedSchemas;
    }

    /**
     * Controls whether to generate JDO-specific class descriptors.
     * 
     * @param generateJdoDescriptors True if JDP class descriptors should be generated
     */
    public void setGenerateJdoDescriptors(final boolean generateJdoDescriptors) {
        _generateJdoDescriptors = generateJdoDescriptors;
    }


    /**
     * Controls whether to generate SAX-1 compliant code.
     * 
     * @param sax1 True if SAX-1 compliant code should be generated.
     */
    public void setSAX1(final boolean sax1) {
        _sax1 = sax1;
    }

    /**
     * Controls whether enumerated type lookup should be performed in a case
     * insensitive manner.
     * 
     * @param caseInsensitive True if enumerated type lookup should be performed in a case
     *        insensitive manner
     */
    public void setCaseInsensitive(final boolean caseInsensitive) {
        _caseInsensitive = caseInsensitive;
    }

    /**
     * Sets the file to use for castor builder properties.
     *
     * @param properties The properties to use.
     */
    public void setProperties(final String properties) {
        _properties = properties;
    }
    
    /**
     * Sets the mode for printing {@link JClass} instances.
     * @param jclassPrinterType The mode for printing {@link JClass} instances.
     */
    public void setJClassPrinterType(final String jclassPrinterType) {
        _jclassPrinterType = jclassPrinterType;
    }

    /**
     * Controls whether a mapping file should (additionally) be generated.

     * @param generateMapping True if a mapping file should be generated.
     */
    public void setGenerateMapping(final boolean generateMapping) {
        _generateMapping = generateMapping;
    }

    /**
     * Configured the code generator. If anything goes wrong during configuration of the
     * Ant task a BuildException will be thrown.
     */
    private void config() {
        // Create Source Generator with appropriate type factory
        if (_types != null) {
            FieldInfoFactory factory;
            try {
                factory = new FieldInfoFactory(_types);
                _sgen = new CastorSourceGeneratorWrapper(factory);
            } catch (Exception e) {
                try {
                    factory = (FieldInfoFactory) Class.forName(_types).newInstance();
                    _sgen = new CastorSourceGeneratorWrapper(factory);
                } catch (Exception e2) {
                    throw new BuildException("Invalid types \"" + _types + "\": " + e.getMessage());
                }
            }
        } else {
            // default
            _sgen = new CastorSourceGeneratorWrapper();
        }

        // Set Line Separator
        String lineSep = System.getProperty("line.separator");
        if (_lineseparator != null) {
            if ("win".equals(_lineseparator)) {
                log("Using Windows style line separation.");
                lineSep = "\r\n";
            } else if ("unix".equals(_lineseparator)) {
                log("Using UNIX style line separation.");
                lineSep = "\n";
            } else if ("mac".equals(_lineseparator)) {
                log("Using Macintosh style line separation.");
                lineSep = "\r";
            } else {
                throw new BuildException(INVALID_LINESEP_MSG);
            }
        }
        _sgen.setLineSeparator(lineSep);

        _sgen.setDestDir(_todir);
        
        // TODO: use reflection to invoke this
        if (_resourcesDirectory != null && _resourcesDirectory.length() > 0) {
            _sgen.setResourceDestination(_resourcesDirectory);
        }

        if (_bindingfile != null) { _sgen.setBinding(_bindingfile); }

        _sgen.setVerbose(_verbose);
        _sgen.setSuppressNonFatalWarnings(!_warnings);

        _sgen.setDescriptorCreation(!_nodesc);
        if (_nodesc) { log(DISABLE_DESCRIPTORS_MSG); }

        _sgen.setCreateMarshalMethods(!_nomarshal);
        if (_nomarshal) { log(DISABLE_MARSHAL_MSG); }

        _sgen.setGenerateImportedSchemas(_generateImportedSchemas);

        _sgen.setSAX1(_sax1);

        _sgen.setCaseInsensitive(_caseInsensitive);
        
        _sgen.setNameConflictStrategy(_nameConflictStrategy);
        
        _sgen.setClassNameConflictResolver(_automaticConflictStrategy);
        
        _sgen.setJClassPrinterType(_jclassPrinterType);
        
        _sgen.setGenerateMappingFile(_generateMapping);
        
        if (_generateJdoDescriptors) {
            callSetterMethodUsingReflection(_sgen, "setJdoDescriptorCreation",
                    boolean.class, new Boolean(_generateJdoDescriptors));
        }

        _sgen.setTestable(_testable);
        if (this._testable) { log(CASTOR_TESTABLE_MSG); }

        // Set Builder Properties;
        if (_properties != null) {
            String filePath = new File(_properties).getAbsolutePath();
            Properties customProperties = new Properties();
            try {
                customProperties.load(new FileInputStream(filePath));
            } catch (FileNotFoundException e) {
                throw new BuildException("Properties file \"" + filePath + "\" not found");
            } catch (IOException e) {
                throw new BuildException("Can't read properties file \"" + filePath + "\": " + e);
            }
            _sgen.setDefaultProperties(customProperties);
        }
    }

    /**
     * Runs source generation. If anything goes wrong during source generation a
     * BuildException will be thrown.
     * 
     * @param filePath an individual Schema to generate code for.
     */
    private void processFile(final String filePath) {
        log("Processing " + filePath);
        try {
            _sgen.generateSource(filePath, _srcpackage);
        } catch (FileNotFoundException e) {
            String message = "XML Schema file \"" + _schemaFile.getAbsolutePath() + "\" not found.";
            log(message);
            throw new BuildException(message);
        } catch (Exception iox) {
            throw new BuildException(iox);
        }
    }

    /**
     * Runs source generation on a XML schema instance pointed to by an URL. If anything 
     * goes wrong during source generation a BuildException will be thrown.
     * 
     * @param schemaURL An URL to an individual XML schema to generate code for.
     */
    private void processURL(final String schemaURL) {
        log("Processing " + schemaURL);
        try {
            InputSource inputSource = new InputSource(schemaURL);
            _sgen.generateSource(inputSource, _srcpackage);
        } catch (FileNotFoundException e) {
            String message = "XML Schema file \"" + _schemaURL + "\" not found.";
            log(message);
            throw new BuildException(message);
        } catch (Exception iox) {
            throw new BuildException(iox);
        }
    }

    /**
     * Public execute method -- entry point for the Ant task.  Loops over all
     * schema that need code generated and creates needed code generators, then
     * executes them. If anything goes wrong during execution of the Ant task a
     * BuildException will be thrown.
     * 
     * @see org.apache.tools.ant.Task#execute()
     */
    public void execute() {
        // Must have something to run the source generator on
        if (_schemaFile == null && _schemaDir == null && _schemaFilesets.size() == 0
                && _schemaURL == null) {
            throw new BuildException(NO_SCHEMA_MSG);
        }

        try {
            config();

            // Run source generator on file
            if (_schemaFile != null) {
                processFile(_schemaFile.getAbsolutePath());
            }

            // Run source generator on all files in directory
            if (_schemaDir != null && _schemaDir.exists() && _schemaDir.isDirectory()) {
                DirectoryScanner ds = this.getDirectoryScanner(_schemaDir);

                String[] files = ds.getIncludedFiles();
                for (int i = 0; i < files.length; i++) {
                    String filePath = _schemaDir.getAbsolutePath() + File.separator + files[i];
                    processFile(filePath);
                }
            }

            // Run source generator on all files in FileSet
            for (int i = 0; i < _schemaFilesets.size(); i++) {
                FileSet fs = (FileSet) _schemaFilesets.elementAt(i);
                DirectoryScanner ds = fs.getDirectoryScanner(getProject());
                File subdir = fs.getDir(getProject());

                String[] files = ds.getIncludedFiles();
                for (int j = 0; j < files.length; j++) {
                    String filePath = subdir.getAbsolutePath() + File.separator + files[j];
                    processFile(filePath);
                }
            }

            // Run source generator on URL for XML schema
            if (_schemaURL != null) {
                processURL(_schemaURL);
            }
        } finally {
            _sgen = null;
        }
    }

    /**
     * Override Castor's SourceGenerator to inject exception handling.
     * Code based on castor-0.9.5.3-xml.jar.
     */
    private final class CastorSourceGeneratorWrapper extends SourceGenerator {
        /**
         * No-arg constructor.
         */
        public CastorSourceGeneratorWrapper() {
            super();
        }

        /**
         * Constructs a source generator with the provided FieldInfoFactory.
         * 
         * @param fieldInfoFactory A FieldInfoFactory to use for collections.
         */
        public CastorSourceGeneratorWrapper(final FieldInfoFactory fieldInfoFactory) {
            super(fieldInfoFactory);
        }

        /**
         * Constructs a source generator with the provided FieldInfoFactory and
         * binding file.
         * 
         * @param fieldInfoFactory a FieldInfoFactory to use for collections.
         * @param extendedBinding binding information for the code generator.
         */
        public CastorSourceGeneratorWrapper(
                final FieldInfoFactory fieldInfoFactory, final ExtendedBinding extendedBinding) {
            super(fieldInfoFactory, extendedBinding);
        }

        /**
         * Generate source. If anything goes wrong during generation of source a
         * BuildException will be thrown.
         *
         * @param source an individual schema to process.
         * @param packageName the package name for generated code.
         * 
         * @see org.exolab.castor.builder.SourceGenerator
         *      #generateSource(org.xml.sax.InputSource, java.lang.String)
         */
        public void generateSource(final InputSource source, final String packageName) {
            Parser parser = null;
            try {
                parser = _internalContext.getParser();
            } catch (RuntimeException e) {
                throw new BuildException("Unable to create SAX parser.", e);
            }
            if (parser == null) {
                throw new BuildException("Unable to create SAX parser.");
            }

            SchemaContext schemaContext = new SchemaContextImpl();
            SchemaUnmarshaller schemaUnmarshaller = null;
            try {
                schemaUnmarshaller = new SchemaUnmarshaller(schemaContext);
            } catch (XMLException e) {
                throw new BuildException("Unable to create schema unmarshaller.", e);
            }

            Sax2ComponentReader handler = new Sax2ComponentReader(schemaUnmarshaller);
            parser.setDocumentHandler(handler);
            parser.setErrorHandler(handler);
            try {
                parser.parse(source);
            } catch (IOException e) {
                String msg = "Can't read input file " + source.getSystemId() + ".\n" + e;
                throw new BuildException(msg, e);
            } catch (SAXException e) {
                String msg = "Can't parse input file " + source.getSystemId() + ".\n" + e;
                throw new BuildException(msg, e);
            }
            Schema schema = schemaUnmarshaller.getSchema();
            try {
                generateSource(schema, packageName);
            } catch (Exception iox) {
                throw new BuildException(iox);
            }
        }
    }
    
    /**
     * Helper method to invoke a setter method on {@link SourceGenerator} that might not 
     * be available due to a version issue.
     * 
     * @param sgen {@link SourceGenerator} instance
     * @param methodName Name of the method
     * @param parameterType Type of the method parameter.
     * @param parameterValue Actual parameter value to be used during method invocation.
     * @throws BuildException If the method cannot be invoked.
     */
    private void callSetterMethodUsingReflection(final SourceGenerator sgen,
            final String methodName, final Class parameterType,
            final Object parameterValue) throws BuildException {
        try {
            Method method = sgen.getClass().getMethod(methodName,
                    new Class[] {parameterType});
            method.invoke(sgen, new Object[] {parameterValue});
        } catch (NoSuchMethodException e) {
            // unable to find method to configure JDO descriptor creation.
        } catch (IllegalArgumentException e) {
            throw new BuildException(PROBLEM_SETTING_JDO_DESC, e);
        } catch (IllegalAccessException e) {
            throw new BuildException(PROBLEM_SETTING_JDO_DESC, e);
        } catch (InvocationTargetException e) {
            throw new BuildException(PROBLEM_SETTING_JDO_DESC, e);
        }
    }



}
