blob: d1fb5d2957111a5d736ffd750a7d41488c98a17d [file] [log] [blame]
/*
* 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.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
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.EntityType;
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.BeanPropertyGetException;
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 Object[] fields;
private boolean[] modified;
private Object rowsetData;
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 = copy(record.modified);
this.fields = copy(record.fields);
this.rowsetData = copy(record.rowsetData);
}
@Override
public DBObject getObject()
{
return record;
}
@Override
public String getObjectInfo()
{
String info = "Record "+record.getRowSet().getName();
if (record.getKeyColumns()==null)
return info;
return 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 = rowsetData;
// done
if (log.isInfoEnabled())
log.info("Rollback for {} performed.", getObjectInfo());
}
@Override
public void discard(Connection conn)
{
/* nothing */
}
private boolean[] copy(boolean[] other)
{
if (other==null)
return null;
boolean[] copy = new boolean[other.length];
for (int i=0; i<copy.length; i++)
{
copy[i] = other[i];
}
return copy;
}
private Object[] copy(Object[] other)
{
if (other==null)
return null;
Object[] copy = new Object[other.length];
for (int i=0; i<copy.length; i++)
{
copy[i] = other[i];
}
return copy;
}
protected Object copy(Object other)
{
if (other==null)
return null;
if (other instanceof Object[])
return copy((Object[])other);
return ClassUtils.copy(other);
}
}
/* 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;
// 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.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
* @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
{
DBRecordBase rec = (DBRecordBase)super.clone();
rec.state = this.state;
if (rec.fields == fields && fields!=null)
rec.fields = fields.clone();
if (rec.modified == modified && modified!=null)
rec.modified = modified.clone();
rec.rowsetData = ClassUtils.copy(this.rowsetData);
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
*/
public abstract boolean isRollbackHandlingEnabled();
/**
* returns true if this record is a new record.
* @return true if this record is a new record
*/
@Override
public EntityType getEntityType()
{
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.
*
* @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.warn("DBRecord.getKey() failed: " + getRowSet().getName() + " primary key value is null!");
}
}
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;
}
// Strings special
if ((value instanceof String) && ((String)value).length()==0)
value = null;
// Is value valid
Object current = fields[index];
if (current==ObjectUtils.NO_VALUE)
throw new FieldValueNotFetchedException(getColumn(index));
// convert
DBColumn column = getColumn(index);
// must convert enums
if (value instanceof Enum<?>)
{ // convert enum
Enum<?> enumVal = ((Enum<?>)value);
boolean numeric = column.getDataType().isNumeric();
value = ObjectUtils.getEnumValue(enumVal, numeric);
}
// Has Value changed?
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(...)
*/
@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
*/
@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
String property = column.getBeanPropertyName();
setRecordValue(column, bean, property);
count++;
}
return count;
}
/**
* Sets record values from the suppied java bean.
* @return true if at least one value has been set sucessfully
*/
@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
*/
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 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
*/
protected void checkUpdateable()
{
if (this.isReadOnly())
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
*/
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);
}
/**
* 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
*/
protected void onFieldChanged(int i)
{
if (log.isDebugEnabled())
log.debug("Record field " + getColumn(i).getName() + " changed to " + String.valueOf(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 property the name of the property
* @param column the column for which to set the record value
*/
protected void setRecordValue(Column column, Object bean, String property)
{
if (StringUtils.isEmpty(property))
property = column.getBeanPropertyName();
try
{ // Get Property Value
PropertyUtilsBean pub = BeanUtilsBean.getInstance().getPropertyUtils();
Object value = pub.getSimpleProperty(bean, property);
// Now, set the record value
set( column, value );
// done
} catch (IllegalAccessException e)
{ log.error(bean.getClass().getName() + ": unable to get property '" + property + "'");
throw new BeanPropertyGetException(bean, property, e);
} catch (InvocationTargetException e)
{ log.error(bean.getClass().getName() + ": unable to get property '" + property + "'");
throw new BeanPropertyGetException(bean, property, e);
} catch (NoSuchMethodException e)
{ log.warn(bean.getClass().getName() + ": no getter available for property '" + property + "'");
throw new BeanPropertyGetException(bean, property, e);
}
}
/**
* 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();
}
}