/*
 *  Copyright 2008 biaoping.yin
 *
 *  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 com.frameworkset.orm.engine.model;

/*
 
 *
 * 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.
 */

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.xml.sax.Attributes;

import com.frameworkset.orm.engine.EngineException;
import com.frameworkset.orm.platform.Platform;
import com.frameworkset.orm.platform.PlatformDefaultImpl;

/**
 * A Class for holding data about a column used in an Application.
 *
 * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
 * @author <a href="mailto:byron_foster@byron_foster@yahoo.com>Byron Foster</a>
 * @author <a href="mailto:mpoeschl@marmot.at>Martin Poeschl</a>
 * @version $Id: Column.java,v 1.28 2005/04/17 10:45:43 tfischer Exp $
 */
public class Column implements Serializable
{
    private static final SchemaType DEFAULT_TYPE = SchemaType.VARCHAR;
    /** Logging class from commons.logging */
    private static Logger log = Logger.getLogger(Column.class);
    private String name;
    private String description;
    private Domain domain = new Domain();
    private String javaName = null;
    private String javaNamingMethod;
    private boolean isNotNull = false;
    private String javaType;
    private Table parentTable;
    private int position;
    private boolean isPrimaryKey = false;
    private boolean isUnique = false;
    private boolean isAutoIncrement = false;
    private List referrers;
    // only one type is supported currently, which assumes the
    // column either contains the classnames or a key to
    // classnames specified in the schema.  Others may be
    // supported later.
    private String inheritanceType;
    private boolean isInheritance;
    private boolean isEnumeratedClasses;
    private List inheritanceList;
    private boolean needsTransactionInPostgres;

    /** generate is... setters for boolean columns if true */
    private boolean correctGetters = false;

    /** class name to do input validation on this column */
    private String inputValidator = null;

    /**
     * Creates a new instance with a <code>null</code> name.
     */
    public Column()
    {
        this(null);
    }

    /**
     * Creates a new column and set the name
     *
     * @param name column name
     */
    public Column(String name)
    {
        this.name = name;
    }

    /**
     * Return a comma delimited string listing the specified columns.
     *
     * @param columns Either a list of <code>Column</code> objects, or
     * a list of <code>String</code> objects with column names.
     */
    public static String makeList(List columns)
    {
        Object obj = columns.get(0);
        boolean isColumnList = (obj instanceof Column);
        if (isColumnList)
        {
            obj = ((Column) obj).getName();
        }
        StringBuffer buf = new StringBuffer((String) obj);
        for (int i = 1; i < columns.size(); i++)
        {
            obj = columns.get(i);
            if (isColumnList)
            {
                obj = ((Column) obj).getName();
            }
            buf.append(", ").append(obj);
        }
        return buf.toString();
    }

    /**
     * Imports a column from an XML specification
     */
    public void loadFromXML(Attributes attrib)
    {
        String dom = attrib.getValue("domain");
        if (StringUtils.isNotEmpty(dom))
        {
            domain = new Domain(getTable().getDatabase().getDomain(dom));
        }
        else
        {
            domain = new Domain(getPlatform().getDomainForSchemaType(DEFAULT_TYPE));
            setType(attrib.getValue("type"));
        }
        //Name
        name = attrib.getValue("name");

        javaName = attrib.getValue("javaName");
        javaType = attrib.getValue("javaType");
        if (javaType != null && javaType.length() == 0)
        {
            javaType = null;
        }

        // retrieves the method for converting from specified name to
        // a java name.
        javaNamingMethod = attrib.getValue("javaNamingMethod");
        if (javaNamingMethod == null)
        {
            javaNamingMethod
                    = parentTable.getDatabase().getDefaultJavaNamingMethod();
        }

        //Primary Key
        String primaryKey = attrib.getValue("primaryKey");
        //Avoid NullPointerExceptions on string comparisons.
        isPrimaryKey = ("true".equals(primaryKey));

        // If this column is a primary key then it can't be null.
        if ("true".equals(primaryKey))
        {
            isNotNull = true;
        }

        // HELP: Should primary key, index, and/or idMethod="native"
        // affect isNotNull?  If not, please document why here.
        String notNull = attrib.getValue("required");
        isNotNull = (notNull != null && "true".equals(notNull));

        //AutoIncrement/Sequences
        String autoIncrement = attrib.getValue("autoIncrement");
        // autoincrement is false per default,
        // except if the column is a primary key
        // and the idMethod is native
        // and the platform's default id Method is identity
        // and autoIncrement is not excplicitly set to false
        isAutoIncrement = ("true".equals(autoIncrement)
                || (isPrimaryKey()
                    && IDMethod.NATIVE.equals(getTable().getIdMethod())
                    && Platform.IDENTITY.equals(
                            getPlatform().getNativeIdMethod())
                    && (!"false".equals(autoIncrement))));
        //Default column value.
        domain.replaceDefaultValue(attrib.getValue("default"));

        domain.replaceSize(attrib.getValue("size"));
        domain.replaceScale(attrib.getValue("scale"));

        inheritanceType = attrib.getValue("inheritance");
        isInheritance = (inheritanceType != null
                && !inheritanceType.equals("false"));

        this.inputValidator = attrib.getValue("inputValidator");
        description = attrib.getValue("description");
    }

    /**
     * Returns table.column
     */
    public String getFullyQualifiedName()
    {
        return (parentTable.getName() + '.' + name);
    }

    /**
     * Get the name of the column
     */
    public String getName()
    {
        return name;
    }

    /**
     * Set the name of the column
     */
    public void setName(String newName)
    {
        name = newName;
    }

    /**
     * Get the description for the Table
     */
    public String getDescription()
    {
        return description;
    }

    /**
     * Set the description for the Table
     *
     * @param newDescription description for the Table
     */
    public void setDescription(String newDescription)
    {
        description = newDescription;
    }

    /**
     * Get name to use in Java sources to build method names.
     *
     * @return the capitalised javaName
     */
    public String getJavaName()
    {
        if (javaName == null)
        {
            List inputs = new ArrayList(2);
            inputs.add(name);
            inputs.add(javaNamingMethod);
            try
            {
                javaName = NameFactory.generateName(NameFactory.JAVA_GENERATOR,
                                                    inputs,false);
            }
            catch (EngineException e)
            {
                log.error(e, e);
            }
        }
        return StringUtils.capitalize(javaName);
    }

    /**
     * Returns the name for the getter method to retrieve the value of this
     * column
     *
     * @return A getter method name for this column.
     * @since 3.2
     */
    public String getGetterName()
    {
        if (("boolean".equalsIgnoreCase(getJavaNative()) && isCorrectGetters()))
        {
            return "is" + StringUtils.capitalize(getJavaName());
        }
        else
        {
            return "get" + StringUtils.capitalize(getJavaName());
        }
    }

    /**
     * Returns the name for the setter method to set the value of this
     * column
     *
     * @return A setter method name for this column.
     * @since 3.2
     */
    public String getSetterName()
    {
        return "set" + StringUtils.capitalize(getJavaName());
    }

    /**
     * Get variable name to use in Java sources (= uncapitalised java name)
     */
    public String getUncapitalisedJavaName()
    {
        return StringUtils.uncapitalize(getJavaName());
    }

    /**
     * Set name to use in Java sources
     */
    public void setJavaName(String javaName)
    {
        this.javaName = javaName;
    }

    /**
     * Get type to use in Java sources
     */
    public String getJavaType()
    {
        return javaType;
    }

    /**
     * Get the location of this column within the table (one-based).
     * @return value of position.
     */
    public int getPosition()
    {
        return position;
    }

    /**
     * Get the location of this column within the table (one-based).
     * @param v  Value to assign to position.
     */
    public void setPosition(int  v)
    {
        this.position = v;
    }

    /**
     * Set the parent Table of the column
     */
    public void setTable(Table parent)
    {
        parentTable = parent;
    }

    /**
     * Get the parent Table of the column
     */
    public Table getTable()
    {
        return parentTable;
    }

    /**
     * Returns the Name of the table the column is in
     */
    public String getTableName()
    {
        return parentTable.getName();
    }

    /**
     * A utility function to create a new column
     * from attrib and add it to this table.
     */
    public Inheritance addInheritance(Attributes attrib)
    {
        Inheritance inh = new Inheritance();
        inh.loadFromXML (attrib);
        addInheritance(inh);

        return inh;
    }

    /**
     * Adds a new inheritance definition to the inheritance list and set the
     * parent column of the inheritance to the current column
     */
    public void addInheritance(Inheritance inh)
    {
        inh.setColumn(this);
        if (inheritanceList == null)
        {
            inheritanceList = new ArrayList();
            isEnumeratedClasses = true;
        }
        inheritanceList.add(inh);
    }

    /**
     * Get the inheritance definitions.
     */
    public List getChildren()
    {
        return inheritanceList;
    }

    /**
     * Determine if this column is a normal property or specifies a
     * the classes that are represented in the table containing this column.
     */
    public boolean isInheritance()
    {
        return isInheritance;
    }

    /**
     * Determine if possible classes have been enumerated in the xml file.
     */
    public boolean isEnumeratedClasses()
    {
        return isEnumeratedClasses;
    }

    /**
     * Return the isNotNull property of the column
     */
    public boolean isNotNull()
    {
        return isNotNull;
    }

    /**
     * Set the isNotNull property of the column
     */
    public void setNotNull(boolean status)
    {
        isNotNull = status;
    }

    /**
     * Return NOT NULL String for this column
     *
     * @return "NOT NULL" if null values are not allowed or an empty String.
     */
    public String getNotNullString()
    {
        return getTable().getDatabase().getPlatform()
                .getNullString(this.isNotNull());
    }

    /**
     * Set if the column is a primary key or not
     */
    public void setPrimaryKey(boolean pk)
    {
        isPrimaryKey = pk;
    }

    /**
     * Return true if the column is a primary key
     */
    public boolean isPrimaryKey()
    {
        return isPrimaryKey;
    }

    /**
     * Set true if the column is UNIQUE
     */
    public void setUnique (boolean u)
    {
        isUnique = u;
    }

    /**
     * Get the UNIQUE property
     */
    public boolean isUnique()
    {
        return isUnique;
    }

    /**
     * Return true if the column requires a transaction in Postgres
     */
    public boolean requiresTransactionInPostgres()
    {
        return needsTransactionInPostgres;
    }

    /**
     * Utility method to determine if this column is a foreign key.
     */
    public boolean isForeignKey()
    {
        return (getForeignKey() != null);
    }

    /**
     * Determine if this column is a foreign key that refers to the
     * same table as another foreign key column in this table.
     */
    public boolean isMultipleFK()
    {
        ForeignKey fk = getForeignKey();
        if (fk != null)
        {
            Iterator fks = parentTable.getForeignKeys().iterator();
            while (fks.hasNext())
            {
                ForeignKey key = (ForeignKey) fks.next();
                if (key.getForeignTableName().equals(fk.getForeignTableName())
                        && !key.getLocalColumns().contains(this.name))
                {
                    return true;
                }
            }
        }

        // No multiple foreign keys.
        return false;
    }

    /**
     * get the foreign key object for this column
     * if it is a foreign key or part of a foreign key
     */
    public ForeignKey getForeignKey()
    {
        return parentTable.getForeignKey(this.name);
    }

    /**
     * Utility method to get the related table of this column if it is a foreign
     * key or part of a foreign key
     */
    public String getRelatedTableName()
    {
        ForeignKey fk = getForeignKey();
        return (fk == null ? null : fk.getForeignTableName());
    }

    /**
     * Utility method to get the related column of this local column if this
     * column is a foreign key or part of a foreign key.
     */
    public String getRelatedColumnName()
    {
        ForeignKey fk = getForeignKey();
        if (fk == null)
        {
            return null;
        }
        else
        {
            return fk.getLocalForeignMapping().get(this.name).toString();
        }
    }

    /**
     * Adds the foreign key from another table that refers to this column.
     */
    public void addReferrer(ForeignKey fk)
    {
        if (referrers == null)
        {
            referrers = new ArrayList(5);
        }
        referrers.add(fk);
    }

    /**
     * Get list of references to this column.
     */
    public List getReferrers()
    {
        if (referrers == null)
        {
            referrers = new ArrayList(5);
        }
        return referrers;
    }

    /**
     * Sets the colunm type
     */
    public void setType(String torqueType)
    {
        SchemaType type = SchemaType.getEnum(torqueType);
        if (type == null)
        {
            log.warn("SchemaType " + torqueType + " does not exist");
            type = Column.DEFAULT_TYPE;
        }

        setType(type);
    }

    /**
     * Sets the colunm type
     */
    public void setType(SchemaType torqueType)
    {
        domain = new Domain(getPlatform().getDomainForSchemaType(torqueType));
        if (torqueType.equals(SchemaType.VARBINARY)
                || torqueType.equals(SchemaType.BLOB))
        {
            needsTransactionInPostgres = true;
        }
    }

    /**
     * Returns the column jdbc type as an object
     *
     * @deprecated the type conversion is handled by the platform package
     *             (since torque 3.2)
     */
    public Object getType()
    {
        return TypeMap.getJdbcType(domain.getType()).getName();
    }

    /**
     * Returns the column type as given in the schema as an object
     */
    public Object getTorqueType()
    {
        return domain.getType().getName();
    }

    /**
     * Utility method to see if the column is a string
     *
     * @deprecated will be removed after the 3.2 release
     */
    public boolean isString()
    {
        return (domain.getType().getName().indexOf ("CHAR") != -1);
    }

    /**
     * Utility method to return the value as an element to be usable
     * in an SQL insert statement. This is used from the SQL loader task
     */
    public boolean needEscapedValue()
    {
        String torqueType = domain.getType().getName();
        return (torqueType != null) && (torqueType.equals("VARCHAR")
                        || torqueType.equals("LONGVARCHAR")
                        || torqueType.equals("DATE")
                        || torqueType.equals("DATETIME")
                        || torqueType.equals("TIMESTAMP")
                        || torqueType.equals("TIME")
                        || torqueType.equals("CHAR")
                        || torqueType.equals("CLOB"));
    }

    /**
     * String representation of the column. This is an xml representation.
     *
     * @return string representation in xml
     */
    public String toString()
    {
        StringBuffer result = new StringBuffer();
        result.append("    <column name=\"").append(name).append('"');

        if (javaName != null)
        {
            result.append(" javaName=\"").append(javaName).append('"');
        }

        if (isPrimaryKey)
        {
            result.append(" primaryKey=\"").append(isPrimaryKey).append('"');
        }

        if (isNotNull)
        {
            result.append(" required=\"true\"");
        }
        else
        {
            result.append(" required=\"false\"");
        }

        result.append(" type=\"").append(domain.getType().getName()).append('"');

        if (domain.getSize() != null)
        {
            result.append(" size=\"").append(domain.getSize()).append('"');
        }

        if (domain.getScale() != null)
        {
            result.append(" scale=\"").append(domain.getScale()).append('"');
        }

        if (domain.getDefaultValue() != null)
        {
            result.append(" default=\"").append(domain.getDefaultValue()).append('"');
        }

        if (isInheritance())
        {
            result.append(" inheritance=\"").append(inheritanceType)
                .append('"');
        }

        // Close the column.
        result.append(" />\n");

        return result.toString();
    }

    /**
     * Returns the size of the column
     */
    public String getSize()
    {
        return domain.getSize();
    }

    /**
     * Set the size of the column
     */
    public void setSize(String newSize)
    {
        domain.setSize(newSize);
    }

    /**
     * Returns the scale of the column
     */
    public String getScale()
    {
        return domain.getScale();
    }

    /**
     * Set the scale of the column
     */
    public void setScale(String newScale)
    {
        domain.setScale(newScale);
    }

    /**
     * Return the size and scale in brackets for use in an sql schema.
     *
     * @return size and scale or an empty String if there are no values
     *         available.
     */
    public String printSize()
    {
        return domain.printSize();
    }

    /**
     * Return a string that will give this column a default value.
     * @deprecated
     */
     public String getDefaultSetting()
     {
         return domain.getDefaultSetting();
     }

    /**
     * Set a string that will give this column a default value.
     */
    public void setDefaultValue(String def)
    {
        domain.setDefaultValue(def);
    }

    /**
     * Get a string that will give this column a default value.
     */
    public String getDefaultValue()
    {
        return domain.getDefaultValue();
    }

    /**
     * Returns the class name to do input validation
     */
    public String getInputValidator()
    {
       return this.inputValidator;
    }

    /**
     * Return auto increment/sequence string for the target database. We need to
     * pass in the props for the target database!
     */
    public boolean isAutoIncrement()
    {
        return isAutoIncrement;
    }

    /**
     * Set the auto increment value.
     * Use isAutoIncrement() to find out if it is set or not.
     */
    public void setAutoIncrement(boolean value)
    {
        isAutoIncrement = value;
    }

    public String getAutoIncrementString()
    {
        if (isAutoIncrement()
                && IDMethod.NATIVE.equals(getTable().getIdMethod()))
        {
            return getPlatform().getAutoIncrement();
        }
        return "";
    }

    /**
     * Set the column type from a string property
     * (normally a string from an sql input file)
     */
    public void setTypeFromString(String typeName, String size)
    {
        String tn = typeName.toUpperCase();
        setType(tn);

        if (size != null)
        {
            domain.setSize(size);
        }

        if (tn.indexOf("CHAR") != -1)
        {
            domain.setType(SchemaType.VARCHAR);
        }
        else if (tn.indexOf("INT") != -1)
        {
            domain.setType(SchemaType.INTEGER);
        }
        else if (tn.indexOf("FLOAT") != -1)
        {
            domain.setType(SchemaType.FLOAT);
        }
        else if (tn.indexOf("DATE") != -1)
        {
            domain.setType(SchemaType.DATE);
        }
        else if (tn.indexOf("TIME") != -1)
        {
            domain.setType(SchemaType.TIMESTAMP);
        }
        else if (tn.indexOf("BINARY") != -1)
        {
            domain.setType(SchemaType.LONGVARBINARY);
        }
        else
        {
            domain.setType(SchemaType.VARCHAR);
        }
    }

    /**
     * Return a string representation of the
     * Java object which corresponds to the JDBC
     * type of this column. Use in the generation
     * of MapBuilders.
     */
    public String getJavaObject()
    {
        return TypeMap.getJavaObject(domain.getType());
    }

    /**
     * Return a string representation of the primitive java type which
     * corresponds to the JDBC type of this column.
     *
     * @return string representation of the primitive java type
     */
    public String getJavaPrimitive()
    {
        return TypeMap.getJavaNative(domain.getType());
    }

    /**
     * Return a string representation of the native java type which corresponds
     * to the JDBC type of this column. Use in the generation of Base objects.
     * This method is used by torque, so it returns Key types for primaryKey and
     * foreignKey columns
     *
     * @return java datatype used by torque
     */
    public String getJavaNative()
    {
        String jtype = TypeMap.getJavaNativeObject(domain.getType());
        if (isUsePrimitive())
        {
            jtype = TypeMap.getJavaNative(domain.getType());
        }

        return jtype;
    }

    /**
     * Return Village asX() method which corresponds to the JDBC type
     * which represents this column.
     */
    public String getVillageMethod()
    {
        String vmethod = TypeMap.getVillageObjectMethod(domain.getType());
        if (isUsePrimitive())
        {
            vmethod = TypeMap.getVillageMethod(domain.getType());
        }

        return vmethod;
    }

    /**
     * Return ParameterParser getX() method which
     * corresponds to the JDBC type which represents this column.
     */
    public String getParameterParserMethod()
    {
        return TypeMap.getPPMethod(domain.getType());
    }

    /**
     * Returns true if the column type is boolean in the
     * java object and a numeric (1 or 0) in the db.
     */
    public boolean isBooleanInt()
    {
        return TypeMap.isBooleanInt(domain.getType());
    }

    /**
     * Returns true if the column type is boolean in the
     * java object and a String ("Y" or "N") in the db.
     */
    public boolean isBooleanChar()
    {
        return TypeMap.isBooleanChar(domain.getType());
    }

    /**
     * Returns true if the column type is boolean in the
     * java object and a Bit ("1" or "0") in the db.
     */
    public boolean isBit()
    {
        return TypeMap.isBit(domain.getType());
    }

    /**
     * returns true, if the columns java native type is an
     * boolean, byte, short, int, long, float, double, char
     */
    public boolean isPrimitive()
    {
        String t = getJavaNative();
        return "boolean".equals(t)
            || "byte".equals(t)
            || "short".equals(t)
            || "int".equals(t)
            || "long".equals(t)
            || "float".equals(t)
            || "double".equals(t)
            || "char".equals(t);
    }

    public boolean isUsePrimitive()
    {
        String s = getJavaType();
        return (s != null && s.equals("primitive"))
            || (s == null && !"object".equals(
               getTable().getDatabase().getDefaultJavaType()));
    }

    /**
     * @return Returns the domain.
     */
    public Domain getDomain()
    {
        return domain;
    }

    /**
     * @param domain The domain to set.
     */
    public void setDomain(Domain domain)
    {
        this.domain = domain;
    }

    private Platform getPlatform()
    {
        try
        {
            Table table = getTable();
            Database dataBase = table.getDatabase();
            Platform platform = dataBase.getPlatform();
            return platform;
        }
        catch (Exception ex)
        {
            log.warn("could not load platform implementation");
        }
        return new PlatformDefaultImpl();
    }

    public String getSqlString()
    {
        StringBuffer sb = new StringBuffer();
        sb.append(getName());
        sb.append(' ');
        sb.append(getDomain().getSqlType());
        if (getPlatform().hasSize(getDomain().getSqlType()))
        {
            sb.append(getDomain().printSize());
            sb.append(' ');
        }
        if (getDomain().getDefaultValue() != null)
        {
            sb.append("default ");
            if (TypeMap.isTextType(getDomain().getType()))
            {
                // TODO: Properly SQL-escape the text.
                sb.append('\'').append(getDefaultValue()).append('\'');
            }
            else
            {
                sb.append(getDefaultValue());
            }
            sb.append(' ');
        }
        
        sb.append(getNotNullString());
        sb.append(' ');
        sb.append(getAutoIncrementString());
        return sb.toString();
    }

    /**
     * Return the correctGetters property of the column
     *
     * @return The currentValue of the correctGetters property.
     * @since 3.2
     */
    public boolean isCorrectGetters()
    {
        return correctGetters;
    }

    /**
     * Set the correctGetters property of the column. If set to true, the
     * column returns is&lt;xxx&gt; as the getter name which is correct for the
     * Bean Specs but incompatible to pre-3.2 releases.
     *
     * @param correctGetters The new value of the correctGetters property.
     * @since 3.2
     */
    public void setCorrectGetters(boolean correctGetters)
    {
        this.correctGetters = correctGetters;
    }

    /**
     * add by biaoping.yin on 2005.9.8
     * @param javaType String
     */
    public void setJavaType(String javaType) {
        this.javaType = javaType;
    }
}
