/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.empire.db;

import java.io.Serializable;
import java.sql.Connection;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.empire.commons.BeanPropertyUtils;
import org.apache.empire.commons.ClassUtils;
import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.commons.Options;
import org.apache.empire.commons.StringUtils;
import org.apache.empire.data.Column;
import org.apache.empire.data.ColumnExpr;
import org.apache.empire.data.Entity;
import org.apache.empire.data.Record;
import org.apache.empire.db.context.DBRollbackHandler;
import org.apache.empire.db.exceptions.FieldReadOnlyException;
import org.apache.empire.db.exceptions.FieldValueNotFetchedException;
import org.apache.empire.db.exceptions.NoPrimaryKeyException;
import org.apache.empire.db.exceptions.RecordReadOnlyException;
import org.apache.empire.exceptions.InvalidArgumentException;
import org.apache.empire.exceptions.NotSupportedException;
import org.apache.empire.exceptions.ObjectNotValidException;
import org.apache.empire.xml.XMLUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * This abstract class provides write access to the fields of a record
 * 
 * The class provides methods that are useful for frontend-form development like
 *   - providing information about the allowed values for a field (field options)
 *   - providing information about whether or not a field is visible to the user    
 *   - providing information about whether or not a field is required (mandantory)    
 *   - providing information about whether or not a field is read-only    
 *   - providing information about whether a particular field value is valid    
 *   - providing information about whether a field was modified since it was read from the database
 *   - providing information about whether the record was modified
 * 
 * Also, field value changes, can be handled using the onFieldChanged event.
 */
public abstract class DBRecordBase extends DBRecordData implements Record, Cloneable, Serializable
{
    private static final long serialVersionUID = 1L;
    
    private static final Logger log  = LoggerFactory.getLogger(DBRecordBase.class);
    
    /**
     * DBRecordRollbackHandler
     * @author doebele
     */
    public static class DBRecordRollbackHandler implements DBRollbackHandler
    {
        // Logger
        private static final Logger log = LoggerFactory.getLogger(DBRecordRollbackHandler.class);
        
        public final DBRecordBase   record;
        
        private final State     state;  /* the original state */
        private final Object[]  fields;
        private boolean[]       modified;
        private final Object    rowsetData;
        private final Map<DBColumn, DBRecordBase> parentRecordMap;
    
        public DBRecordRollbackHandler(DBRecordBase record)
        {
            this.record = record;
            // check
            if (record.state==State.Invalid)
                throw new ObjectNotValidException(record);
            // save state
            this.state = record.state;            
            this.modified   = ClassUtils.copy(record.modified);
            this.fields     = ClassUtils.copy(record.fields);
            this.rowsetData = ClassUtils.copySmart(record.rowsetData);
            this.parentRecordMap = ClassUtils.copySmart(record.parentRecordMap);
        }

        @Override
        public DBObject getObject()
        {
            return record;
        }

        @Override
        public String getObjectInfo()
        {
            String info = "Record "+record.getRowSet().getName();
            if (record.getKeyColumns()==null)
                return info;
            return StringUtils.concat(info, ":", StringUtils.arrayToString(record.getKey()));
        }

        @Override
        public void combine(DBRollbackHandler successor)
        {
            if (record!=successor.getObject())
                throw new InvalidArgumentException("successor", successor);
            // combine now
            DBRecordRollbackHandler s = (DBRecordRollbackHandler)successor;
            log.info("combining rollback state for record {}/{}", record.getRowSet().getName(), StringUtils.arrayToString(record.getKey(), "|"));
            if (s.modified==null)
            {
                return; // not modified!
            }
            // Make sure we have a modified array 
            if (modified==null)
                modified = new boolean[fields.length];
            // special case Timestamp
            DBRowSet rowset = record.getRowSet();
            DBColumn tsColumn = record.getRowSet().getTimestampColumn();
            // copy
            for (int i=0; i<fields.length; i++)
            {   // ignore timestamp and key columns
                DBColumn column = record.getColumn(i);
                if (column==tsColumn || rowset.isKeyColumn(column))
                    continue;
                // copy modified fields
                if (s.modified[i]==false)
                    continue;
                // field was modified
                fields[i] = s.fields[i];
                if (modified!=null)
                    modified[i] = true;
            }
        }

        @Override
        public void rollback(Connection conn)
        {
            // rollback
            record.state = this.state;
            record.fields = this.fields;
            record.modified = this.modified;
            record.rowsetData = this.rowsetData;
            record.parentRecordMap = this.parentRecordMap;
            // done
            if (log.isInfoEnabled())
                log.info("Rollback for {} performed.", getObjectInfo());
        }

        @Override
        public void discard(Connection conn)
        {
            /* nothing */
        }
    }
  
    /* Record state enum */
    public enum State
    {
    	Invalid,
    /*	Empty,  not used! */
    	Valid,
    	Modified,
    	New;

    	/* Accessors */
    	boolean isLess(State other)
    	{
    		return this.ordinal()<other.ordinal();
    	}
    	boolean isEqualOrMore(State other)
    	{
    		return this.ordinal()>=other.ordinal();
    	}
    }
    
    // This is the record data
    private State           state;
    private Object[]        fields;
    private boolean[]       modified;
    Object                  rowsetData; // Special Rowset Data (usually null)
    protected boolean       validateFieldValues;
    protected boolean       allowReadOnlyUpdate;
    
    // Parent-Record-Map for deferred identity setting 
    private Map<DBColumn, DBRecordBase> parentRecordMap;
    
    /**
     * Internal constructor for DBRecord
     * May be used by derived classes to provide special behaviour
     */
    protected DBRecordBase()
    {
        // init
        this.state = State.Invalid;
        this.fields = null;
        this.modified = null;
        this.rowsetData = null;
        this.validateFieldValues = true;
        this.allowReadOnlyUpdate = false;
        this.parentRecordMap = null;
    }

    /**
     * helper to check if the object is valid
     * @throws ObjectNotValidException if the object is not valid
     */
    protected void checkValid()
    {
        if (!isValid())
            throw new ObjectNotValidException(this);
    }

    /**
     * helper to check if the object is valid
     * @param fieldIndex the field index
     * @throws ObjectNotValidException if the object is not valid
     */
    protected void checkValid(int fieldIndex)
    {
        if (!isValid())
            throw new ObjectNotValidException(this);
        // Check index
        if (fieldIndex < 0 || fieldIndex>= fields.length)
            throw new InvalidArgumentException("index", fieldIndex);
    }
    
    /**
     * Closes the record by releasing all resources and resetting the record's state to invalid.
     */
    public void close()
    {
        // clear fields
        fields = null;
        modified = null;
        rowsetData = null;
        // change state
        if (state!=State.Invalid)
            changeState(State.Invalid);
        // done
        onRecordChanged();
    }
    
    /** {@inheritDoc} */
    @Override
    public DBRecordBase clone()
    {
        try {
            // clone this record
            DBRecordBase rec = (DBRecordBase)super.clone();
            rec.initData(this);
            return rec;
        } catch (CloneNotSupportedException e) {
            log.error("Unable to clone record.", e);
            return null;
        }
    }

    /**
     * Returns the DBRowSet object.
     * @return the DBRowSet object
     */
    public abstract DBRowSet getRowSet();
    
    /**
     * Returns whether or not RollbackHandling is enabled for this record
     * @return true if rollback handling is enabled or false otherwise
     */
    public abstract boolean isRollbackHandlingEnabled(); 

    /**
     * returns true if this record is a new record.
     * @return true if this record is a new record
     */
    @Override
    public Entity getEntity()
    {
        return getRowSet();
    }

    /**
     * Returns the current DBDatabase object.
     * 
     * @return the current DBDatabase object
     */
    @SuppressWarnings("unchecked")
    @Override
	public DBDatabase getDatabase()
    {
        return getRowSet().getDatabase();
    }

    /**
     * Returns the record state.
     * 
     * @return the record state
     */
    public State getState()
    {
        return state;
    }

    /**
     * Returns true if the record is valid.
     * 
     * @return true if the record is valid
     */
    @Override
    public boolean isValid()
    {
        return (state != State.Invalid);
    }

    /**
     * Returns true if the record is valid.
     * 
     * @return true if the record is valid
     */
    @Override
    public boolean isReadOnly()
    {
        if (!isValid())
            return true;
        DBRowSet rowset = getRowSet();
        return (rowset==null || !rowset.isUpdateable());
    }

    /**
     * Returns true if the record is modified.
     * 
     * @return true if the record is modified
     */
    @Override
    public boolean isModified()
    {
        return (state.isEqualOrMore(State.Modified));
    }

    /**
     * Returns true if this record is a new record.
     * 
     * @return true if this record is a new record
     */
    @Override
    public boolean isNew()
    {
        return (state == State.New);
    }

    /**
     * Returns true if this record is a existing record (valid but not new).
     * This may be used from expression language instead of the not allowed property "new" 
     * 
     * @return true if this record is a existing record (valid but not new).
     */
    public boolean isExists()
    {
        return (state == State.Valid || state == State.Modified);
    }

    /**
     * Returns the number of the columns.
     * 
     * @return the number of the columns
     */
    @Override
    public int getFieldCount()
    {
        return (fields != null) ? fields.length : 0;
    }

    /**
     * Returns the index value by a specified DBColumnExpr object.
     * 
     * @return the index value
     */
    @Override
    public int getFieldIndex(ColumnExpr column)
    {
        return getRowSet().getColumnIndex(column);
    }

    /**
     * Returns the index value by a specified column name.
     * 
     * @return the index value
     */
    @Override
    public int getFieldIndex(String column)
    {
        List<DBColumn> columns = getRowSet().getColumns();
        for (int i = 0; i < columns.size(); i++)
        {
            DBColumn col = columns.get(i);
            if (col.getName().equalsIgnoreCase(column))
                return i;
        }
        // not found
        return -1;
    }

    /**
     * Implements the Record Interface getColumn method.<BR>
     * Internally calls getDBColumn()
     * @return the Column at the specified index 
     */
    @Override
    public DBColumn getColumn(int index)
    {
        return getRowSet().getColumn(index);
    }
    
    /**
     * Returns true if the field was modified.
     * 
     * @param index the field index
     *  
     * @return true if the field was modified
     */
    public boolean wasModified(int index)
    {
        checkValid(index);
        // Check modified
        if (modified == null)
            return false;
        return modified[index];
    }
    
    /**
     * Returns true if the field was modified.
     * 
     * @return true if the field was modified
     */
    @Override
    public final boolean wasModified(Column column)
    {
        return wasModified(getFieldIndex(column));
    }

    /**
     * Returns true if any of the given fields was modified.
     * @param columns the columns to check
     * @return true if any of the given fields were modified or false otherwise
     */
    public final boolean wasAnyModified(Column... columns)
    {
        for (Column c : columns)
        {
            if (wasModified(getFieldIndex(c)))
                return true;
        }
        return false;
    }

    /**
     * Returns whether or not values are checked for validity when calling setValue().
     * If set to true validateValue() is called to check validity
     * @return true if the validity of values is checked or false otherwise
     */
    public boolean isValidateFieldValues() 
    {
        return validateFieldValues;
    }

    /**
     * Set whether or not values are checked for validity when calling setValue().
     * If set to true validateValue() is called to check validity, otherwise not.
     * @param validateFieldValues flag whether to check validity
     */
    public void setValidateFieldValues(boolean validateFieldValues) 
    {
        this.validateFieldValues = validateFieldValues;
    }

	/**
     * returns an array of key columns which uniquely identify the record.
     * @return the array of key columns if any
     */
    @Override
    public Column[] getKeyColumns()
    {
        return getRowSet().getKeyColumns();
    }

    /**
     * Returns a array of key columns by a specified DBRecord object.
     * @return a array of key columns
     */
    @Override
    public Object[] getKey()
    {
        // Check Columns
        Column[] keyColumns = getKeyColumns();
        if (keyColumns == null || keyColumns.length==0)
            throw new NoPrimaryKeyException(getRowSet());
        // create the key
        Object[] key = new Object[keyColumns.length];
        for (int i = 0; i < keyColumns.length; i++)
        {
            key[i] = get(keyColumns[i]);
            if (key[i] == null)
            {   // Primary Key not set
                log.info("Record key for {}: column {} is null!", getRowSet().getName(), keyColumns[i].getName());
            }
        }
        return key;
    }

    /**
     * Returns the value for the given column or null if either the index is out of range or the value is not valid (see {@link DBRecordBase#isValueValid(int)})
     * @return the index value
     */
    @Override
    public Object getValue(int index)
    {   // Check state
        checkValid(index);
        // Special check for NO_VALUE 
        if (fields[index] == ObjectUtils.NO_VALUE)
            throw new FieldValueNotFetchedException(getColumn(index));
        // Return field value
        return fields[index];
    }
    
    /**
     * Returns whether a field value is provided i.e. the value is not DBRowSet.NO_VALUE<BR>
     * This function is only useful in cases where records are partially loaded.<BR>
     * 
     * @param index the filed index
     *  
     * @return true if a valid value is supplied for the given field or false if value is {@link ObjectUtils#NO_VALUE}  
     */
    public boolean isValueValid(int index)
    {   // Check state
        checkValid(index);
        // Special check for NO_VALUE
        return (fields[index] != ObjectUtils.NO_VALUE);
    }

    /**
     * Gets the possbile Options for a field in the context of the current record.
     * 
     * @param column the database field column
     *  
     * @return the field options 
     */
    public Options getFieldOptions(DBColumn column)
    {
        // DBColumn col = ((colexpr instanceof DBColumn) ? ((DBColumn) colexpr) : colexpr.getSourceColumn());
        return column.getOptions();
    }

    /**
     * Gets the possbile Options for a field in the context of the current record.<BR>
     * Same as getFieldOptions(DBColumn)
     * @return the Option
     */
    @Override
    public final Options getFieldOptions(Column column)
    {
        return getFieldOptions((DBColumn)column); 
    }
    
    /**
     * validates all modified values of a record
     */
    public void validateAllValues()
    {
        checkValid();
        // Modified
        if (modified == null)
            return; // nothing to do
        // check for field
        for (int index=0; index<fields.length; index++)
        {   // Modified or No value?
            if (modified[index]==false || fields[index]==ObjectUtils.NO_VALUE)
                continue;
            // Auto-generated ?
            DBColumn column = getColumn(index);
            if (column.isAutoGenerated())
                continue;
            // validate this one
            fields[index] = validateValue(column, fields[index]);
        }
    }

    /**
     * Sets the value of a column in the record.
     * The functions checks if the column and the value are valid and whether the
     * value has changed.
     * 
     * @param index the index of the column
     * @param value the value
     */
    @Override
    public void setValue(int index, Object value)
    {
        checkValid(index);
        // check updatable
        checkUpdateable();
        // Special case ParentRecord
        if (value instanceof DBRecordBase)
        {   // Special case: Value contains parent record
            setParentRecord(getColumn(index), (DBRecordBase)value);
            return;
        }
        // Is value valid
        Object current = fields[index]; 
        if (current==ObjectUtils.NO_VALUE)
            throw new FieldValueNotFetchedException(getColumn(index));
        // convert
        DBColumn column = getColumn(index);
        // Has Value changed?
        if (value!= null)
            value = ObjectUtils.convertValue(column.getDataType(), value);
        if (ObjectUtils.compareEqual(current, value))
        {   // value has not changed!
            return; 
        }
        // Check whether we can change this field
        if (!allowFieldChange(column))
        {   // Read Only column may be set
            throw new FieldReadOnlyException(column);
        }
        // Is Value valid?
        if (isValidateFieldValues())
        {   // validate
            Object validated = validateValue(column, value);
            if (value != validated)
            {   // Value has been converted, check again
                if (ObjectUtils.compareEqual(current, validated))
                    return; 
                // user converted value
                value = validated;
            }
        }
        // Init original values
        modifyValue(index, value, true);
    }

    /**
     * Deprecated Renamed to set(...)   
     * @param column a DBColumn object
     * @param value the value
     * @return returns self (this)
     */
    @Deprecated
    public DBRecordBase setValue(Column column, Object value)
    {
        return set(column, value);
    }

    /**
     * Sets the value of a column in the record.
     * The functions checks if the column and the value are valid and whether the
     * value has changed.
     * 
     * @param column a DBColumn object
     * @param value the value
     * @return returns self (this)
     */
    @Override
    public DBRecordBase set(Column column, Object value)
    {   
        setValue(getFieldIndex(column), value);
        return this;
    }

    /**
     * Validates a value before it is set in the record.
     * By default, this method simply calls column.validate()
     * @param column the column that needs to be changed
     * @param value the new value
     */
    @Override
    public Object validateValue(Column column, Object value)
    {
    	return column.validateValue(value);
    }
    
    /**
     * returns whether a field is visible to the client or not
     * <P>
     * May be overridden to implement context specific logic.
     * @param column the column which to check for visibility
     * @return true if the column is visible or false if not 
     */
    @Override
    public boolean isFieldVisible(Column column)
    {
    	// Check value
        int index = getRowSet().getColumnIndex(column);
        if (index<0)
        {   // Column not found
            log.warn("Column {} does not exist for record of {}", column.getName(), getRowSet().getName());
        }
        return (index>=0 && isValueValid(index));
    }
    
    /**
     * returns whether a field is read only or not
     * 
     * @param column the database column 
     * 
     * @return true if the field is read only
     */
    @Override
    public boolean isFieldReadOnly(Column column)
    {
        DBRowSet rowset = getRowSet();
    	if (getFieldIndex(column)<0)
            throw new InvalidArgumentException("column", column);
    	// Check key column 
        if (isValid() && !isNew() && rowset.isKeyColumn((DBColumn)column))
        	return true;
        // Ask RowSet
        return (rowset.isColumnReadOnly((DBColumn)column));
    }
    
    /**
     * returns whether a field is required or not
     * 
     * @param column the database column 
     * 
     * @return true if the field is required
     */
    @Override
    public boolean isFieldRequired(Column column)
    {
    	if (getRowSet().getColumnIndex(column)<0)
            throw new InvalidArgumentException("column", column);
        // from column definition
        return (column.isRequired());
    }
    
    /**
     * Sets record values from the supplied java bean.
     * 
     * @return true if at least one value has been set successfully 
     */
    @Override
    public int setRecordValues(Object bean, Collection<Column> ignoreList)
    {
        // Add all Columns
        int count = 0;
        for (int i = 0; i < getFieldCount(); i++)
        { // Check Property
            DBColumn column = getColumn(i);
            if (column.isReadOnly())
                continue;
            if (ignoreList != null && ignoreList.contains(column))
                continue; // ignore this property
            // Get Property Name
            setRecordValue(column, bean);
            count++;
        }
        return count;
    }

    /**
     * Sets record values from the suppied java bean.
     * @return true if at least one value has been set successfully
     */
    @Override
    public final int setRecordValues(Object bean)
    {
        return setRecordValues(bean, null);
    }
    
    /**
     * For DBMS with IDENTITY-columns defer setting the parent-id until the record is inserted
     * The parent record must have a one-column primary key
     * @param parentIdColumn the column for which to set the parent
     * @param record the parent record to be set for the column
     */
    public void setParentRecord(DBColumn parentIdColumn, DBRecordBase record)
    {
        checkValid();
        // check column
        checkParamNull("parentIdColumn", parentIdColumn);
        // check updateable
        checkUpdateable();
        // remove
        if (record==null)
        {   // clear parent 
            if (parentRecordMap!=null)
                parentRecordMap.remove(parentIdColumn);
            set(parentIdColumn, null);
            return;
        }
        // set key or record
        Object[] key = record.getKey();
        if (key.length!=1)
            throw new NotSupportedException(this, "setParentRecord");
        if (key[0]==null)
        {   // preserve until later
            if (parentRecordMap==null)
                parentRecordMap = new HashMap<DBColumn, DBRecordBase>(1);
            // add record to map
            log.info("Deffering setting of {} until the record is saved!", parentIdColumn.getName());
            parentRecordMap.put(parentIdColumn, record);
        }
        else
        {   // set directly
            int index = getFieldIndex(parentIdColumn);
            Object id = getValue(index);
            if (!ObjectUtils.compareEqual(id, key[0]))
            {   // set parent-id
                modifyValue(index, key[0], true);
            }
        }
    }

    /**
     * Compares the record to another one
     * @param other the record to compare this record with
     * @return true if it is the same record (but maybe a different instance)
     */
    public boolean isSame(DBRecordBase other)
    {   // check valid
        if (!isValid() || !other.isValid())
            return false;
        // compare table
        if (!getRowSet().isSame(other.getRowSet()))
            return false;
        // compare key
        Object[] key1 = getKey();
        Object[] key2 = other.getKey();
        return ObjectUtils.compareEqual(key1, key2);
    }

    /**
     * This function set the field descriptions to the the XML tag.
     * 
     * @return the number of column descriptions added to the element
     */
    @Override
    public int addXmlMeta(Element parent)
    {
        checkValid();
        // Add Field Description
        int count = 0;
        List<DBColumn> columns = getRowSet().getColumns();
        for (int i = 0; i < columns.size(); i++)
        { // Add Field
            DBColumn column = columns.get(i);
            if (isFieldVisible(column)==false)
                continue;
            column.addXml(parent, 0);
            count++;
        }
        return count;
    }

    /**
     * Add the values of this record to the specified XML Element object.
     * 
     * @param parent the XML Element object
     * @return the number of row values added to the element
     */
    @Override
    public int addXmlData(Element parent)
    {
        checkValid();
        // set row key
        Column[] keyColumns = getKeyColumns();
        if (keyColumns != null && keyColumns.length > 0)
        { // key exits
            if (keyColumns.length > 1)
            { // multi-Column-id
                StringBuilder buf = new StringBuilder();
                for (int i = 0; i < keyColumns.length; i++)
                { // add
                    if (i > 0)
                        buf.append("/");
                    buf.append(getString(keyColumns[i]));
                }
                parent.setAttribute("id", buf.toString());
            } 
            else
                parent.setAttribute("id", getString(keyColumns[0]));
        }
        // row attributes
        if (isNew())
            parent.setAttribute("new", "1");
        // Add all children
        int count = 0;
        List<DBColumn> columns = getRowSet().getColumns();
        for (int i = 0; i < fields.length; i++)
        { // Read all
            DBColumn column = columns.get(i);
            if (isFieldVisible(column)==false)
                continue;
            // Add Field Value
            String name = column.getName();
            if (fields[i] != null)
                XMLUtil.addElement(parent, name, getString(i));
            else
                XMLUtil.addElement(parent, name).setAttribute("null", "yes"); // Null-Value
            // increase count
            count++;
        }
        return count;
    }

    /**
     * Returns a XML document with the field description an values of this record.
     * 
     * @return the new XML Document object
     */
    @Override
    public Document getXmlDocument()
    {
        checkValid();
        // Create Document
        DBXmlDictionary xmlDic = getXmlDictionary();
        Element root = XMLUtil.createDocument(xmlDic.getRowSetElementName());
        DBRowSet rowset = getRowSet();
        if (rowset.getName() != null)
            root.setAttribute("name", rowset.getName());
        // Add Field Description
        if (addXmlMeta(root)>0)
        {   // Add row Values
            addXmlData(XMLUtil.addElement(root, xmlDic.getRowElementName()));
        }
        // return Document
        return root.getOwnerDocument();
    }
    
    /**
     * This function provides direct access to the record fields.<BR>
     * This method is used internally be the RowSet to fill the data.<BR>
     * @return an array of field values
     */
    protected Object[] getFields()
    {
        return fields;
    }
    
    /**
     * changes the state of the record
     * @param newState the new record state
     */
    protected void changeState(State newState)
    {
        this.state = newState;
    }
    
    /**
     * Factory function to create  createRollbackHandler();
     * @return the DBRollbackHandler
     */
    protected DBRollbackHandler createRollbackHandler()
    {
        return new DBRecordRollbackHandler(this);
    }
    
    /**
     * This method is used internally by the RowSet to initialize the record's properties
     * @param newRecord flag whether the record is new (non-existing) in the database
     */
    protected void initData(boolean newRecord)
    {
        // Init rowset
        DBRowSet rowset = getRowSet();
        int colCount = rowset.getColumns().size();
        if (fields==null || fields.length!=colCount)
            fields = new Object[colCount];
        else
        {   // clear fields
            for (int i=0; i<fields.length; i++)
                if (fields[i]!=ObjectUtils.NO_VALUE)
                    fields[i]=null;
        }
        // Set State
        this.modified = null;
        this.rowsetData = null;
        changeState((rowset==null ? State.Invalid : (newRecord ? State.New : State.Valid)));
    }
    
    /**
     * This method may be used to copy the record in order to preserve its state
     * @param other the record to be copied
     */
    protected void initData(DBRecordBase other)
    {
        // rowsets must match!
        if (other==null || !other.getRowSet().equals(getRowSet()))
            throw new InvalidArgumentException("other", other);
        // init fields from other record
        this.state = other.state;
        this.fields = ClassUtils.copy(other.fields);
        this.modified = ClassUtils.copy(other.modified);
        this.rowsetData = ClassUtils.copySmart(other.rowsetData);
        this.parentRecordMap = ClassUtils.copySmart(other.parentRecordMap);
        this.validateFieldValues = other.validateFieldValues;
        this.allowReadOnlyUpdate = other.allowReadOnlyUpdate;
    }
    
    /**
     * This method is used internally to indicate that the record update has completed<BR>
     * This will set change the record's state to Valid
     */
    protected void updateComplete()
    {
        // Change state
        this.modified = null;
        changeState(State.Valid);
    }
    
    /**
     * Checks whether the record is updateable  
     * If its read-only a RecordReadOnlyException is thrown 
     * @throws RecordReadOnlyException exception thrown if record cannot be updated
     */
    protected void checkUpdateable()
    {
        if (this.isReadOnly())
        {   // Check allowReadOnlyUpdate
            if (!allowReadOnlyUpdate)
                throw new RecordReadOnlyException(this);
        }
    }
    
    /**
     * Checks whether or not this field can be changed at all.
     * Note: This is not equivalent to isFieldReadOnly() 
     * @param column the column that needs to be changed
     * @return true if it is possible to change this field for this record context
     */
    protected boolean allowFieldChange(DBColumn column)
    {
        // Check auto generated
        if (column.isAutoGenerated() && (!isNew() || !isNull(column)))
            return false;
        // Check key Column
        if (!isNew() && getRowSet().isKeyColumn(column))
            return false;
        // done
        return true;
    }

    /**
     * Modifies a column value bypassing all checks made by setValue.
     * Use this to explicitly set invalid values i.e. for temporary storage.
     * 
     * @param index index of the column
     * @param value the column value
     * @param fireChangeEvent flag whether to fire the onFieldChanged event
     */
    protected void modifyValue(int index, Object value, boolean fireChangeEvent)
    {   // Check valid
        checkValid(index);
        // modified state array
        if (modified == null)
        {   modified = new boolean[fields.length];
            for (int j = 0; j < fields.length; j++)
                modified[j] = false;
        }
        // set value and modified
        fields[index] = value;
        modified[index] = true;
        // set record state
        if (state.isLess(State.Modified))
            changeState(State.Modified);
        // field changed event
        if (fireChangeEvent)
            onFieldChanged(index);
    }

    /**
     * Sets a fields modified status
     * @param index the field index
     * @param modifiedFlag flag indicating whether or not the field was modified
     */
    protected void setFieldModified(int index, boolean modifiedFlag)
    {   // Check valid
        checkValid(index);
        if (modified == null && !modifiedFlag)
            return; // Nothing to do
        // modified state array
        if (modified == null)
        {   modified = new boolean[fields.length];
            for (int j = 0; j < fields.length; j++)
                modified[j] = false;
        }
        // set value and modified
        modified[index] = modifiedFlag;
        // set record state
        boolean recordModified = modifiedFlag;
        for (int j = 0; j < fields.length && !recordModified; j++)
            recordModified |= modified[j];
        // Change record state if necessary
        if (recordModified && state.isLess(State.Modified))
            changeState(State.Modified);
        else if (recordModified==false && state==State.Modified)
            changeState(State.Valid);
    }
    
    /**
     * Override this to do extra handling when the record changes
     */
    protected void onRecordChanged()
    {
        if (log.isTraceEnabled() && isValid())
            log.trace("Record has been changed");
        // Remove rollback (but not when close() is called!)
        if (fields!=null && isRollbackHandlingEnabled())
            getContext().removeRollbackHandler(this);
    }
    
    /**
     * Override this to get notified when a field value changes
     * @param i the field index
     */
    protected void onFieldChanged(int i)
    {
        if (log.isDebugEnabled())
            log.debug("Record field {} changed to {}", getColumn(i).getName(), fields[i]);
    }

    /**
     * set a record value from a particular bean property.
     * <P>
     * For a property called FOO this is equivalent of calling<BR>
     *     setValue(column, bean.getFOO())
     * <P>
     * @param bean the Java Bean from which to read the value from
     * @param column the column for which to set the record value
     */
    protected void setRecordValue(Column column, Object bean)
    {
        // check
        if (column==null)
            throw new InvalidArgumentException("column", column);
        if (bean==null)
            throw new InvalidArgumentException("bean", column);
        // the property name
        String property = column.getBeanPropertyName();
        // Get Property Value
        Object value = BeanPropertyUtils.getProperty(bean, property);
        // Now, set the record value
        set( column, value ); 
    }
    
    /**
     * For DBMS with IDENTITY-columns the deferred parent-keys are set by this functions
     * The parent records must have been previously set using setParentRecord
     */
    protected void assignParentIdentities()
    {
        // Check map
        if (parentRecordMap==null)
            return;
        // Apply map
        for (Map.Entry<DBColumn, DBRecordBase> e : parentRecordMap.entrySet())
        {
            DBColumn parentIdColumn = e.getKey();
            Object keyValue = e.getValue().getKey()[0];
            if (keyValue==null)
                throw new ObjectNotValidException(e.getValue());
            // Set key
            log.info("Deffered setting of {} to {}!", parentIdColumn.getName(), keyValue);
            set(parentIdColumn, keyValue);
        }
        parentRecordMap.clear();
    }
    
    /**
     * helper function to check if a given field index corresponds to one of the given columns
     * @param index the field index
     * @param column one or more columns to check
     * @return true if the index is for one of the columns or false otherwise
     */
    protected boolean isColumn(int index, DBColumn... column)
    {
        if (index < 0 || index >= fields.length)
            throw new InvalidArgumentException("index", index);
        if (column==null)
            throw new InvalidArgumentException("column", column);
        Column col = getColumn(index);
        for (int i=0; i<column.length; i++)
        {   // compare
            if (col==column[i])
                return true;
        }
        // not found
        return false; 
    }
    
    /**
     * returns the DBXmlDictionary that should used to generate XMLDocuments<BR>
     * @return the DBXmlDictionary
     */
    protected DBXmlDictionary getXmlDictionary()
    {
        return DBXmlDictionary.getInstance();
    }
    
}
