/*
 * $Header: /home/cvspublic/jakarta-slide/src/stores/org/apache/slide/store/ojb/tools/DDLGeneratorTask.java,v 1.2 2005/02/06 10:41:33 cvillegas Exp $
 * $Revision: 1.2 $
 * $Date: 2005/02/06 10:41:33 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.apache.slide.store.ojb.tools;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.util.StringUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import java.util.List;
import java.util.Vector;
import java.util.Iterator;
import java.util.HashSet;
import java.util.Properties;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.log.LogSystem;
import org.apache.velocity.runtime.RuntimeServices;

/**
 * Generates a DDL script, the schema, for a given database from a
 * Torque XML database schema.
 * The script is generated using a Velocity template which is loaded from
 * the classpath. The name of the velocity template is databasename + ".vm".
 * A properties file databasename + ".properties" is also read from the classpath
 * to set database specific settings. At the moment only the mappings from JDBC
 * types to native types are used.
 *
 */
public class DDLGeneratorTask extends Task implements LogSystem {

    private Vector schemaFiles = new Vector();
    private String outputPattern;
    private Vector databases;
    private HashSet seen = new HashSet();
    private VelocityEngine engine;
    private static final String DB = "%{database}";
    private static final String IX = "%{index}";
    private Vector elemTables = new Vector();
    private long schemaLastModified = 0;
    
    /**
     * @param schema The database schema in Torque XML format
     */
    public void setSchema(String schemaArg) {
        Vector schema = StringUtils.split(schemaArg, ',');
        for(int i=0; i<schema.size(); i++) {
            String item = schema.get(i).toString().trim();
            File sfile = new File(item);
            if ( !sfile.isAbsolute() )
                sfile = new File(getProject().getBaseDir(), item);
            schemaFiles.add(sfile);
            long lm = sfile.lastModified();
            if ( lm > schemaLastModified )
                schemaLastModified = lm;
        }
    }
    
    /**
     * The pattern to generate the output filename. The variables %{database}
     * with the target database name and %{index} with the database index in the
     * list of targets are substituted to generate the output filename.
     *
     * @param pattern sets the pattern for the generated output file name.
     */
    public void setOutputPattern(String pattern) {
        this.outputPattern = pattern;
    }

    /**
     * use pattern to generate File for output
     */
    private File getOutputFile(int index) {
        String database = (String)databases.get(index);
        String oname = StringUtils.replace(outputPattern, DB, database);
        oname = StringUtils.replace(oname, IX, Integer.toString(index));
        File ofile = new File(oname);
        if ( !ofile.isAbsolute() )
            ofile = new File(getProject().getBaseDir(), oname);
        return ofile;
    }
    
    /**
     * comma separated list of target databases.
     */
    public void setTargetDatabases(String targets) {
        if ( targets == null )
            return;
        databases = StringUtils.split(targets, ',');
    }
    
    /**
     * Generate the DDL script for the target databases.
     * 
     * @see org.apache.tools.ant.Task#execute()
     */
    public void execute() throws BuildException {
        if (schemaFiles.size() == 0 ) {
            throw new BuildException( "Must specify an schema attribute" );
        }
        if (outputPattern == null) {
            throw new BuildException( "Must specify an output attribute" );
        }
        if ( databases == null )
            log("Nothing to do, no target databases");
        try {
            for(Iterator si=schemaFiles.iterator();si.hasNext();) {
                File sfile = (File) si.next();
                Document doc = new SAXBuilder().build(sfile);
                List tables = doc.getRootElement().getChildren("table");
                reorder(tables);
                elemTables.addAll(tables);
            }
            initVelocity();
            for (int i = 0; i < databases.size(); i++) {
                processDatabase(i);
            }            
         } catch (Exception e) {
            e.printStackTrace();
            throw new BuildException(e);
        }
    }

    private void processDatabase(int di) throws BuildException {
        String database = databases.get(di).toString().trim();
        File output = getOutputFile(di);
        try {
            if ( output.exists() && output.lastModified() > schemaLastModified )
                return;
            log("Generating " + database + " DDL --> " + output.getName());
            Properties props = loadProperties(database);
            Vector tables = new Vector();
            for(Iterator t=elemTables.iterator();t.hasNext();) {
                Element elemTable = (Element)t.next();
                Table table = new Table();
                table.name = elemTable.getAttributeValue("name");
                Index pk = null;
                for(Iterator c=elemTable.getChildren("column").iterator();c.hasNext();) {
                    Element elemCol = (Element)c.next();
                    Column col = new Column();
                    col.name = elemCol.getAttributeValue("name");
                    Boolean isPK = Boolean.valueOf(elemCol.getAttributeValue("primaryKey"));
                    if ( isPK.booleanValue() ) {
                        if ( pk == null ) {
                            pk = new Index();
                            pk.name = table.name + "_PK";
                        }
                        pk.columns.add(col);
                    }
                    try {
                        col.size = Integer.parseInt(elemCol.getAttributeValue("size"));
                    } catch (Exception e) {
                    }
                    col.type = props.getProperty(elemCol.getAttributeValue("type"));
                    int varlen = col.size > 0 ? col.size : 250;
                    col.type = StringUtils.replace(col.type, "(n)", "(" + varlen + ")");
                    col.isNotNull = Boolean.valueOf(elemCol.getAttributeValue("required"));
                    table.columns.add(col);
                }
                table.primaryKey = pk;
                for(Iterator fks=elemTable.getChildren("foreign-key").iterator(); fks.hasNext();) {
                    Element elemFk = (Element)fks.next();
                    ForeignKey fk = new ForeignKey();
                    fk.foreignTable = elemFk.getAttributeValue("foreignTable");
                    Element ref = elemFk.getChild("reference");
                    fk.localKey = ref.getAttributeValue("local");
                    fk.foreignKey = ref.getAttributeValue("foreign");
                    // sometime we have duplicate declarations
                    if ( findForeignKey(table.foreignKeys, fk.localKey) == null )
                        table.foreignKeys.add(fk);
                }

                for(Iterator ixs=elemTable.getChildren("unique").iterator();ixs.hasNext();) {
                    Element elemIndex = (Element)ixs.next();
                    Index index = new Index();
                    index.isUnique = true;
                    index.name = elemIndex.getAttributeValue("name");
                    for(Iterator ucs=elemIndex.getChildren("unique-column").iterator();ucs.hasNext();) {
                        Element elemCol = (Element)ucs.next();
                        Column col = findColumn(table.columns, elemCol.getAttributeValue("name"));
                        if ( col != null )
                            index.columns.add(col);
                    }
                    table.indices.add(index);
                }
                tables.add(table);
            }
            VelocityContext context = new VelocityContext();
            context.put("eol", System.getProperty("line.separator"));
            context.put("dquot", "\"");
            context.put("quot", "'");
            context.put("strings", new Strings());
            context.put("tables", tables);
            context.put("props", props);
            Vector dropTables = new Vector();
            for(int i=tables.size()-1; i>=0; i--) {
                dropTables.add(tables.get(i));
            }
            context.put("droptables", dropTables);
            OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(output));

            engine.mergeTemplate(database + ".vm", context, writer);
            writer.flush();
            writer.close();
        } catch (Exception e) {
            e.printStackTrace();
            throw new BuildException(e);
        }
    } 

    private Properties loadProperties(String database) throws BuildException {
        String propFile = database + ".properties";
        try {
            Properties props = new Properties();
            props.load(getClass().getResourceAsStream("/" + propFile));
            return props;
        } catch (Exception e) {
            throw new BuildException(propFile + " not found", e);
        }
    }

    private Column findColumn(List columns, String name) {
        for(Iterator i=columns.iterator();i.hasNext();) {
            Column col = (Column)i.next();
            if ( col.name.equals(name) )
                return col;
        }
        return null;
    }

    private ForeignKey findForeignKey(List keys, String localKey) {
        for(Iterator i=keys.iterator();i.hasNext();) {
            ForeignKey fk = (ForeignKey)i.next();
            if ( fk.localKey.equals(localKey) )
                return fk;
        }
        return null;
    }
    
    /**
     * Reorder the table elements so that tables in foreign key
     * constraints appear before tables that declares them. 
     *
     * @param tables a JDOM list containing the table elements
     *
     */
    private void reorder(List tables) {
        seen.clear();
        for(int i=0; i<tables.size(); i++) {
            i = reorderTable(tables, i);
        }
    } 
    
    private int reorderTable(List tables, int i) {
        Element table = (Element)tables.get(i);
        List fks = table.getChildren("foreign-key");
        for (Iterator k=fks.iterator();k.hasNext();) {
            Element fk = (Element)k.next();
            String ft = fk.getAttributeValue("foreignTable");
            if ( ! seen.contains(ft) ) {
                Element dep = findTable(tables, i+1, ft);
                if ( dep != null ) {
                    tables.add(i, dep.detach());
                    i = reorderTable(tables, i) + 1;
                }
            }
        }
        seen.add(table.getAttributeValue("name"));
        return i;
    }
    
    private Element findTable(List tables, int start, String name) {
        for(int i=start; i<tables.size();i++) {
            Element table = (Element)tables.get(i);
            if ( table.getAttributeValue("name").equals(name) )
                return table;
        }
        return null;
    }
    
    // classes for passing data to Velocity template 
    
    public class Table {
        String name;
        Vector columns = new Vector();
        Index primaryKey;
        Vector indices = new Vector();
        Vector foreignKeys = new Vector();
        
        public String getName() {
            return name;
        }

        public List getColumns() {
            return columns;
        }

        public List getForeignKeys() {
            return foreignKeys;
        }

        public List getIndices() {
            return indices;
        }

        public Index getPrimaryKey() {
            return primaryKey;
        }
    }

    public class Column {
        String name;
        String type;
        int size;
        Boolean isNotNull = Boolean.FALSE;

        public String getName() {
            return name;
        }

        public String getType() {
            return type;
        }

        public int getSize() {
            return size;
        }

        public String printSize() {
            String s = "";
            if ( size > 0 )
                s = "(" + size + ")";
            return s;
        }
        
        public Boolean isNotNull() {
            return isNotNull;
        }
    }

    public class ForeignKey {
        String localKey;
        String foreignTable;
        String foreignKey;

        public String getLocalKey() {
            return localKey;
        }

        public String getForeignTable() {
            return foreignTable;
        }

        public String getForeignKey() {
            return foreignKey;
        }
    }

    public class Index {
        String name;
        Vector columns = new Vector();
        boolean isUnique;

        public String getName() {
            return name;
        }
        
        public List getColumns() {
            return columns;
        }

        public boolean isUnique() {
            return isUnique;
        }
    }

    public class Strings {

        /**
         * chops m characters from end of string after removing whitespace
         */
        public String chop(String src, int m) {
            src = trimRight(src);
            if ( src.length()-m < 0 )
                return src;
            return src.substring(0, src.length()-m);
        }

        public String trimRight(String src) {
            if ( src == null )
                return src;
            int i = src.length();
            if ( i == 0 )
                return src;
            while(i>0) {
                if ( !Character.isWhitespace(src.charAt(--i)) )
                     break;
            }
            return src.substring(0, i+1);
        }
    }
    
    private void initVelocity() throws BuildException {
        if ( engine != null )
            return;
        engine = new VelocityEngine();
        engine.setProperty(VelocityEngine.RUNTIME_LOG_LOGSYSTEM, this);
        engine.setProperty(VelocityEngine.PARSER_POOL_SIZE, new Integer(5));
        // load templates from classpath
        engine.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader" );
        engine.setProperty(VelocityEngine.RESOURCE_LOADER, "class");
        try {
            engine.init();
        } catch (Exception e) {
            throw new BuildException("Could not initialize Velocity Engine", e);
        }
    }

    // Velocity LogSystem implementation
    
    public void init( RuntimeServices rs ) throws Exception {
    }

    public void logVelocityMessage(int level, String message)
    {
        switch (level) {
            case LogSystem.WARN_ID:
                log( message, Project.MSG_WARN );
                break;
            case LogSystem.INFO_ID:
                // make Velocity quiet
                log(message, Project.MSG_DEBUG);
                break;
            case LogSystem.DEBUG_ID:
                log(message, Project.MSG_VERBOSE);
                break;
            case LogSystem.ERROR_ID:
                log(message, Project.MSG_ERR);
                break;
            default:
                log(message, Project.MSG_DEBUG);
                break;
        }
    }
    
}
