/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.orm.activerecord;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.json.JSONObject;
import com.scooterframework.common.exception.GenericException;
import com.scooterframework.common.exception.InvalidOperationException;
import com.scooterframework.common.logging.LogUtil;
import com.scooterframework.common.util.Converters;
import com.scooterframework.common.util.StringUtil;
import com.scooterframework.common.util.Util;
import com.scooterframework.common.util.WordUtil;
import com.scooterframework.common.validation.ValidationResults;
import com.scooterframework.orm.sqldataexpress.config.DatabaseConfig;
import com.scooterframework.orm.sqldataexpress.exception.BaseSQLException;
import com.scooterframework.orm.sqldataexpress.object.ColumnInfo;
import com.scooterframework.orm.sqldataexpress.object.OmniDTO;
import com.scooterframework.orm.sqldataexpress.object.RESTified;
import com.scooterframework.orm.sqldataexpress.object.RowData;
import com.scooterframework.orm.sqldataexpress.object.RowInfo;
import com.scooterframework.orm.sqldataexpress.object.TableInfo;
import com.scooterframework.orm.sqldataexpress.processor.DataProcessor;
import com.scooterframework.orm.sqldataexpress.processor.DataProcessorTypes;
import com.scooterframework.orm.sqldataexpress.service.SqlService;
import com.scooterframework.orm.sqldataexpress.service.SqlServiceClient;
import com.scooterframework.orm.sqldataexpress.service.SqlServiceConfig;
import com.scooterframework.orm.sqldataexpress.util.SqlExpressUtil;
import com.scooterframework.transaction.ImplicitTransactionManager;
import com.scooterframework.transaction.TransactionManagerUtil;
/**
* <p>
* ActiveRecord represents a dynamic record in a particular database. Column
* meta data of the table are retrieved before the ActiveRecord instance
* is created. The following is an example for <tt>posts</tt> table.
* </p>
*
* <blockquote><pre>
* public class Post extends ActiveRecord {
* }
* </pre></blockquote>
*
* <p>
* If not specified, the table name an ActiveRecord class maps to is the
* plural form of the class name. And the database is the default database
* connection defined in the <tt>database.properties</tt> file.
* </p>
*
* <p>
* Subclass should override the <tt>getTableName()</tt> method or
* the <tt>getConnectionName()</tt> method if non-default behavior is required.
* For example, the follow code defines a Post class that links with
* <tt>all_posts</tt> table in the <tt>blog_test</tt> database.
* </p>
*
* <blockquote><pre>
* public class Post extends ActiveRecord {
* public String getTableName() {
* return "all_posts";
* }
*
* public String getConnectionName() {
* return "blog_test";
* }
* }
* </pre></blockquote>
*
* <p>
* To establish relations with other models, all subclasses must implement the
* <tt>registerRelations()</tt> method by calling proper relationship setup
* methods: <tt>hasOne</tt>, <tt>belongsTo</tt>, <tt>hasMany</tt>,
* <tt>hasManyThrough</tt>, etc. For example:
* </p>
*
* <blockquote><pre>
* public class Post extends ActiveRecord {
* public void registerRelations() {
* hasMany("comments");
* }
* }
* </pre></blockquote>
*
* <p>
* To modify an attribute of a record, you need to use one of the
* <tt>setData</tt> methods. Otherwise, the change may not be saved.
* </p>
*
*<h3>Specifying conditions</h3>
*
* <p>Conditions are often supplied in find, update and delete related methods.
* They help to construct <tt>WHERE</tt> clause of a SQL statement. Conditions
* can be provided in three ways:</p>
* <ol>
* <li><tt>conditionsSQL</tt> String</li>
* <li><tt>conditionsSQL</tt> String and <tt>conditionsSQLData</tt> Array</li>
* <li><tt>conditionsSQL</tt> String and <tt>conditionsSQLData</tt> Map</li>
* </ol>
*
* <p><tt>conditionsSQL</tt> String specifies a SQL fragment which is used in
* where clause. For example:</p>
* <blockquote><pre>
* conditionsSQL: "id in (1, 3, 5, 7) and content like '%Java%'"
* </pre></blockquote>
*
* <p><tt>conditionsSQL</tt> String and <tt>conditionsSQLData</tt> Array allows
* dynamic data in a SQL fragment. Each element in the array is corresponding
* to the value to be set to the <tt>conditionsSQL</tt>. Internally, the array
* is converted to a map with key starting from 1 for each element in the
* array. For example:</p>
* <blockquote><pre>
* conditionsSQL: "first_name=? OR last_name=?"
* conditionsSQLData array: {"John", "Doe"}
* </pre></blockquote>
*
* <p><tt>conditionsSQL</tt> String and <tt>conditionsSQLData</tt> Map allows
* dynamic data in a SQL fragment. For example:</p>
* <blockquote><pre>
* conditionsSQL: "first_name=?1 OR last_name=?2"
* conditionsSQLData map: 1=John, 2=Doe
* </pre></blockquote>
*
*<h3>Querying APIs</h3>
*
* <p>The following chainable methods are introduced for retrieving
* data from the database. </p>
*<ul>
* <li><tt>where</tt>: specifies where clause in the SQL query</li>
* <li><tt>groupBy</tt>: specifies group-by clause in the SQL query</li>
* <li><tt>having</tt>: specifies having clause in the SQL query</li>
* <li><tt>orderBy</tt>: specifies order-by clause in the SQL query</li>
* <li><tt>limit</tt>: specifies number of records for each retrieval</li>
* <li><tt>offset</tt>: specifies number of records to skip in a retrieval</li>
* <li><tt>page</tt>: specifies the starting page in a pagination</li>
* <li><tt>includes</tt>: specifies models to eager loaded. See below for more details.</li>
*</ul>
*
* <p>Each method above allows us to retrieve data in a chainable way.
* For example, in a PetClinic application: </p>
* <blockquote><pre>
* To retrieve a pet named Leo:
* ActiveRecord Leo = Pet.where("name='Leo'").getRecord();
*
* The SQL equivalent of the above is:
* SELECT * FROM pets WHERE name = 'Leo'
*
* To retrieve all pets owned by owners with id 6 and 10, order by latest birth date:
* List pets = Pet.where("owner_id IN (6, 10)").orderBy("birth_date DESC").getRecords();
*
* The SQL equivalent of the above is:
* SELECT * FROM pets WHERE owner_id IN (6, 10) ORDER BY birth_date DESC
*
* To retrieve a pet owner along with all the pets he/she has and each pet's type in one query (eager loading):
* ActiveRecord owner6 = Owner.where("owners.id=6").includes("pets=>visits, pets=>type").getRecord();
*
* The SQL equivalent of the above is:
* SELECT OWNERS.ID AS OWNERS_ID, OWNERS.FIRST_NAME AS OWNERS_FIRST_NAME,
* OWNERS.LAST_NAME AS OWNERS_LAST_NAME, OWNERS.ADDRESS AS OWNERS_ADDRESS,
* OWNERS.CITY AS OWNERS_CITY, OWNERS.TELEPHONE AS OWNERS_TELEPHONE,
* PETS.ID AS PETS_ID, PETS.NAME AS PETS_NAME, PETS.BIRTH_DATE AS PETS_BIRTH_DATE,
* PETS.TYPE_ID AS PETS_TYPE_ID, PETS.OWNER_ID AS PETS_OWNER_ID,
* VISITS.ID AS VISITS_ID, VISITS.PET_ID AS VISITS_PET_ID,
* VISITS.VISIT_DATE AS VISITS_VISIT_DATE, VISITS.DESCRIPTION AS VISITS_DESCRIPTION,
* OWNERS_PETS.ID AS OWNERS_PETS_ID, OWNERS_PETS.NAME AS OWNERS_PETS_NAME,
* OWNERS_PETS.BIRTH_DATE AS OWNERS_PETS_BIRTH_DATE,
* OWNERS_PETS.TYPE_ID AS OWNERS_PETS_TYPE_ID,
* OWNERS_PETS.OWNER_ID AS OWNERS_PETS_OWNER_ID,
* TYPES.ID AS TYPES_ID, TYPES.NAME AS TYPES_NAME
* FROM OWNERS LEFT OUTER JOIN PETS ON OWNERS.ID=PETS.OWNER_ID
* LEFT OUTER JOIN VISITS ON PETS.ID=VISITS.PET_ID
* LEFT OUTER JOIN PETS OWNERS_PETS ON OWNERS.ID=OWNERS_PETS.OWNER_ID
* LEFT OUTER JOIN TYPES ON OWNERS_PETS.TYPE_ID=TYPES.ID
* WHERE OWNERS.ID = 6
* </pre></blockquote>
*
*<h3>Specifying <tt>options</tt> (<tt>properties</tt>)</h3>
*
* <p>Please notice that in all finder methods, <tt>options</tt> are
* replaced by chainable querying methods described above. <tt>options</tt>
* are used in specifying relations.</p>
*
* <p><tt>options</tt> can be either a string or a map.</p>
*
* <p>In an option string, each name-value pair is separated by ';'
* character, while within each name-value pair, name and value strings
* are separated by ':' character. For example, an option string like the
* following: </p>
* <blockquote><pre>
* conditions_sql: id in (1, 2, 3); include: category, user;
* order_by: first_name, salary DESC; cascade: delete
* </pre></blockquote>
*
* is equivalent to a HashMap with the following entries:
* <blockquote><pre>
* key value
* -------------- -----
* conditions_sql => id in (1, 2, 3)
* include => category, user
* order_by => first_name, salary desc
* cascade => delete
* </pre></blockquote>
*
* <p>Options string or map are used in association
* methods such as <tt>hasMany</tt> and <tt>hasManyThrough</tt>. The
* following is a list of allowed properties:
* <tt>model</tt>, <tt>mapping</tt>, <tt>finder_sql</tt>,
* <tt>conditions_sql</tt>, <tt>include</tt>, <tt>join_type</tt>,
* <tt>order_by</tt>, <tt>unique</tt>,
* <tt>cascade</tt>. Please note that <tt>model</tt>
* property is only used in setting up relations.
* </p>
*
*<h3>Dynamic finders</h3>
*
* <p>Dynamic finders simulate Ruby-on-Rails's finder methods. These methods
* are <tt>findAllBy</tt>, <tt>findFirstBy</tt>, and <tt>findLastBy</tt>. The
* first input parameter of these methods is <tt>columns</tt> which is a string
* of column names linked by <tt>_and_</tt>, such as:
* <blockquote><pre>
* {columnName}_and_{columnName}_and_...
* </pre></blockquote>
*
* <p>A client can call this method as follows:</p>
* <blockquote><pre>
* Employee.findAllBy("firstName_and_lastName_and_age", {"John", "Doe", Integer.valueOf(29)});
* Employee.findAllBy("city", {"LA"});
* </pre></blockquote>
*
*<h3>Validators</h3>
*
* <p>See java doc of {@link com.scooterframework.common.validation.Validators Validators} class.</p>
*
* @author (Fei) John Chen
*/
public class ActiveRecord extends ActiveRecordClass
implements RESTified, Serializable {
/**
* <p>Constructs an instance of <tt>ActiveRecord</tt>.</p>
*
* <p>The created instance is based on meta info for table as returned
* by <tt>getTableName()</tt> and database connection name as returned
* by <tt>getConnectionName()</tt>.</p>
*
* <p>This constructor will populate the meta info of the record and its
* table. The table name defaults to short class name. For example, if
* the model class name is <tt>com.example.models.User</tt>, the default
* table name for this class is <tt>posts</tt> or <tt>CRM_users_US</tt>
* if there are prefix <tt>CRM_</tt> and suffix <tt>_US</tt> for all
* tables as specified in configuration file.</p>
*
* <p>See description of {@link #ActiveRecord(String connectionName, String tableName)} constructor.</p>
*/
public ActiveRecord() {
connectionName = getConnectionName();
tableName = getTableName();
initialize(connectionName, tableName);
}
/**
* <p>Constructs an instance of ActiveRecord.</p>
*
* <p>The created instance is based on meta info for <tt>tableName</tt> and
* database connection name as returned by <tt>getConnectionName()</tt>.</p>
*
* <p>
* This constructor will populate the meta info of the record and its
* table. </p>
*
* <p>
* <tt>tableName</tt> is table name of the record. The the prefix and
* suffix of the database table name may be removed. For example, if a
* table name is <tt>CRM_users_US</tt> which has a prefix
* <tt>CRM_</tt> and a suffix <tt>_US</tt>, the slim table name
* used here can just be <tt>users</tt>. </p>
*
* <p>See description of {@link #ActiveRecord(String connectionName, String tableName)} constructor.</p>
*
* @param tableName table name of the record.
*/
public ActiveRecord(String tableName) {
if (tableName == null)
throw new IllegalArgumentException("Table name cannot be null in ActiveRecord(String).");
this.connectionName = getConnectionName();
this.tableName = DatabaseConfig.getInstance().getFullTableName(tableName);
initialize(connectionName, this.tableName);
}
/**
* <p>Constructs an instance of ActiveRecord for a specific database
* connection.</p>
*
* <p>
* This constructor will populate the meta info of the record and its
* table. </p>
*
* <p>
* <tt>connectionName</tt> is database connection name. For example,
* for the following entry in <tt>database.properties</tt> file, the
* <tt>connectionName</tt> is <tt>jpetstore</tt>:</p>
*
* <pre>
database.connection.jpetstore=\
driver=oracle.jdbc.driver.OracleDriver,\
url=jdbc:oracle:thin:@127.0.0.1:1521:jpetstore,\
username=scott,\
password=tiger
* </pre>
*
* <p>
* <tt>tableName</tt> is table name of the record. The the prefix and
* suffix of the database table name may be removed. For example, if a
* table name is <tt>CRM_users_US</tt> which has a prefix
* <tt>CRM_</tt> and a suffix <tt>_US</tt>, the table name used here can
* just be <tt>users</tt>. </p>
*
* @param connectionName database connection name.
* @param tableName table name of the record.
*/
public ActiveRecord(String connectionName, String tableName) {
if (connectionName == null)
throw new IllegalArgumentException("Connection name cannot be null in ActiveRecord(String, String).");
if (tableName == null)
throw new IllegalArgumentException("Table name cannot be null in ActiveRecord(String, String).");
this.connectionName = connectionName;
this.tableName = DatabaseConfig.getInstance().getFullTableName(tableName);
initialize(connectionName, this.tableName);
}
/**
* <p>Returns the database connection name associated with this record.</p>
*
* <p>By default, this method returns the default database
* connection name as defined in <tt>database.properties</tt> file.
* Subclass can override this method to link this ActiveRecord class to
* other database connection names defined in the
* <tt>database.properties</tt> file.</p>
*
* @return database connection name
*/
public String getConnectionName() {
return (connectionName != null)?connectionName:getDefaultConnectionName();
}
/**
* <p>Returns the primary key string the record. This method is the same as
* the {@link #getRestfulId getRestfulId} method.</p>
*
* @return primary key String
*/
public String getPK() {
return getRestfulId();
}
/**
* <p>
* Returns the restified id of the resource.
* </p>
*
* <p>By default, this method returns a string of the primary key value of
* the record. If the primary key is a composite key, a separator
* ({@link com.scooterframework.orm.sqldataexpress.config.DatabaseConfig#PRIMARY_KEY_SEPARATOR}) is used
* to link values of the key fields. The order of the fields of a composite
* primary key is defined by the {@link #getRestfulIdNames getRestfulIdNames} method.
* </p>
*
* <p>If the underlying data does not have primary key, then all columns of
* the data are used to compute the <tt>id</tt>.</p>
*
* <p>Subclass may override this method if a customized string format is
* required.</p>
*
* @return id String
*/
public String getRestfulId() {
return (rowData != null)?rowData.getRestfulId():null;
}
/**
* Returns column names corresponding to the restified id value returned by
* the {@link #getRestfulId getRestfulId} method. By default, the order of the field names
* are obtained from the {@link #getPrimaryKeyNames getPrimaryKeyNames} method.
* Subclass may override this method if a customized string array is required.
*
* @return string array of fields behind the id
*/
public String[] getRestfulIdNames() {
String[] results = null;
if (rowInfo != null) {
results = rowInfo.getPrimaryKeyColumnNames();
if (results == null) results = rowInfo.getColumnNames();
}
return results;
}
/**
* Returns the data map for the restified id. By default, the keys in the
* map are primary key column names in lower case.
*
* @return map of restified id data
*/
public Map<String, Object> getRestfulIdMap() {
return (rowData != null)?rowData.getRestfulIdMap():(new HashMap<String, Object>());
}
/**
* <p>Sets the id value of the resource. The format of the id string must
* follow the pattern of the corresponding id config. If the id is backed
* by a composite primary key, a separator
* ({@link com.scooterframework.orm.sqldataexpress.config.DatabaseConfig#PRIMARY_KEY_SEPARATOR})
* must be used to link values of each primary key column.</p>
*
* <pre>
* Examples:
* id string id config array description
* --------- --------------- -------
* 0001 [id] an order with id 0001
* 0001-99 [order_id, id] an item with id 99 on the order with id 0001
*
* </pre>
*
* @param id
*/
public void setRestfulId(String id) {
if (id == null) throw new IllegalArgumentException("Input id is null.");
String[] ids = Converters.convertStringToStringArray(id, DatabaseConfig.PRIMARY_KEY_SEPARATOR, false);
String[] fields = getRestfulIdNames();
if (ids.length != fields.length)
throw new IllegalArgumentException("Input id does not match field name(s) for the id.");
int total = ids.length;
for (int i = 0; i < total; i++) {
setData(fields[i], ids[i]);
}
}
/**
* Returns record meta data.
*
* @return RowInfo object of a table row
* @see com.scooterframework.orm.sqldataexpress.object.RowInfo
*/
public RowInfo getRowInfo() {
return rowInfo;
}
/**
* Returns plain data of a row in a table.
*
* @return an array of objects
*/
public Object[] getFields() {
return rowData.getFields();
}
/**
* Returns plain data for a column index.
*
* @param index column index like 0, 1, 2, ...
* @return a data object for the column
*/
public Object getField(int index) {
return rowData.getField(index);
}
/**
* Returns plain data for a field.
*
* @param fieldName name of a model field
* @return a data object for the field
*/
public Object getField(String fieldName) {
verifyExistenceOfField(fieldName);
Object data = null;
if (isExtraField(fieldName)) {
data = getExtraFieldData(fieldName);
}
else {
data = rowData.getField(fieldName);
}
return data;
}
/**
* Returns a map of column name and value pairs.
*
* @param columnNames names of a database table column
* @return a map of column name and value pairs
*/
public Map<String, Object> getFields(List<String> columnNames) {
Map<String, Object> edMap = getExtraFieldData(columnNames);
Map<String, Object> rdMap = rowData.getDataMap(columnNames);
Map<String, Object> dMap = new HashMap<String, Object>();
if (edMap != null && edMap.size() > 0) dMap.putAll(edMap);
if (rdMap != null && rdMap.size() > 0) dMap.putAll(rdMap);
return dMap;
}
/**
* Sets primary key columns for the record
*/
public void setPrimaryKey(Set<String> primaryKeyNames) {
if (isFreezed()) throw new InvalidOperationException(this, "setPrimaryKey", "freezed");
rowInfo.setPrimaryKeyColumns(primaryKeyNames);
}
/**
* <p>Sets a column to be readonly.</p>
*
* <p>Sometimes columns like "entry_user, entry_dt, update_user, update_dt or xxx_count"
* are readonly. They will be updated by other database mechanisms.</p>
*/
public void setReadOnlyColumn(String readOnlyColumnName) {
if (isFreezed()) throw new InvalidOperationException(this, "setReadOnlyColumn", "freezed");
rowInfo.setReadOnlyColumn(readOnlyColumnName);
}
/**
* <p>Sets read-only columns.</p>
*
* <p>Sometimes columns like "entry_user, entry_dt, update_user, update_dt"
* are readonly. They will be updated by other database mechanisms.</p>
*/
public void setReadOnlyColumns(Set<String> readOnlyColumnNames) {
if (isFreezed()) throw new InvalidOperationException(this, "setReadOnlyColumns", "freezed");
rowInfo.setReadOnlyColumns(readOnlyColumnNames);
}
/**
* Checks whether a column is a readonly column.
*
* @param colName the column name to be checked.
* @return true if the column is readonly
*/
public boolean isReadOnlyColumn(String colName) {
return rowInfo.isReadOnlyColumn(colName);
}
/**
* Checks whether a column is a required column. Data for a required
* column cannot be set to null.
*
* @param colName the column name to be checked.
* @return true if the column is required.
*/
public boolean isRequiredColumn(String colName) {
return rowInfo.isRequiredColumn(colName);
}
/**
* Indicates if the current record has been modified and unsaved to database.
*/
public boolean isDirty() {
return dirty;
}
/**
* Indicates if the current record is a new record--not in database yet.
*/
public boolean isNewRecord() {
return !existInDatabase;
}
/**
* Indicates if the current record is freezed
*/
public boolean isFreezed() {
return freezed;
}
/**
* Freezes the current record.
*/
public void freeze() {
freezed = true;
}
/**
* Indicates if the current instance is a home instance.
*/
public boolean isHomeInstance() {
return isHomeInstance;
}
/**
* Sets this instance as a home instance.
*/
void setAsHomeInstance() {
isHomeInstance = true;
}
/**
* <p>Creates the record in database and returns it.</p>
*
* <p>This method is the same as {@link #create(boolean) create(true)}.</p>
*/
public ActiveRecord create() {
return create(true);
}
/**
* <p>Creates the record in database and returns it.</p>
*
* <p>If <tt>changedOnly</tt> is <tt>true</tt>, only changed fields are
* included in SQL query.</p>
*
* <p>
* This method calls {@link #beforeCreate() beforeCreate()} before the real
* create, and {@link #afterCreate() afterCreate()} after the create
* execution.
* </p>
*
* @param changedOnly true if only changed fields are included in SQL query
* @return a new ActiveRecord instance.
*/
public ActiveRecord create(boolean changedOnly) {
ImplicitTransactionManager tm = TransactionManagerUtil.getImplicitTransactionManager();
ActiveRecord r = null;
try {
tm.beginTransactionImplicit();
beforeCreate();
r = internal_create(changedOnly);
afterCreate();
tm.commitTransactionImplicit();
ActiveRecordUtil.getGateway(getClass()).getModelCacheClient().clearCache("create");
}
catch(BaseSQLException bdex) {
tm.rollbackTransactionImplicit();
throw bdex;
}
finally {
tm.releaseResourcesImplicit();
}
return r;
}
/**
* <p>Finds the record based on the current primary key values.</p>
*
* <p>If there is no primary key defined for the model, data from all
* columns will be used as retrieving conditions.</p>
*/
private void find() {
if (isFreezed()) throw new InvalidOperationException(this, "find", "freezed");
if (rowData == null) return;
//populate a Map with primary key values
String[] pkNames = rowInfo.getPrimaryKeyColumnNames();
if (pkNames == null || pkNames.length == 0) pkNames = rowInfo.getColumnNames();
int size = pkNames.length;
Map<String, Object> inputs = new HashMap<String, Object>();
for (int i=0; i< size; i++) {
String columnName = pkNames[i];
Object columnData = rowData.getField(columnName);
inputs.put(columnName, columnData);
}
ActiveRecord refreshedRecord = ActiveRecordUtil.getGateway(getClass()).findFirst(inputs);
if (refreshedRecord != null) {
this.rowData = refreshedRecord.rowData;
}
}
/**
* Updates a single field and saves the record.
*
* @param field a field name
* @param value value of the field
*/
public void updateField(String field, Object value) {
if (isFreezed()) throw new InvalidOperationException(this, "updateField", "freezed");
Map<String, Object> m = new HashMap<String, Object>();
m.put(field, value);
updateFields(m);
}
/**
* Updates all the fields contained in the map for a record (row) in database
*
* @param fieldData a map of field and its data pairs
*/
public void updateFields(Map<String, ?> fieldData) {
if (isFreezed()) throw new InvalidOperationException(this, "updateFields", "freezed");
setData(fieldData);
update(true);
}
/**
* Updates counters
*
* Counters map contains the names of the fields to update as keys and
* the amount to update the field by as values.
*
* @param counters
*/
public void updateCounters(Map<String, ? extends Number> counters) {
updateFields(counters);
}
/**
* Increments counter field by 1.
*
* @param btr BelongsToRecordRelation
*/
public void incrementCounter(BelongsToRecordRelation btr) {
incrementCounter(((BelongsToRelation)btr.getRelation()).getCounterCacheName());
}
/**
* Increments counter field by 1.
*
* @param counterFieldName
*/
public void incrementCounter(String counterFieldName) {
incrementCounter(counterFieldName, +1);
}
/**
* Increments counter field by amount.
*
* @param btr BelongsToRecordRelation
* @param amount
*/
public void incrementCounter(BelongsToRecordRelation btr, int amount) {
incrementCounter(((BelongsToRelation)btr.getRelation()).getCounterCacheName(), amount);
}
/**
* Increments counter field by amount.
*
* @param counterFieldName
* @param amount
*/
public void incrementCounter(String counterFieldName, int amount) {
Object oldCount = getField(counterFieldName);
int oldIntValue = Util.getSafeIntValue(oldCount);
Map<String, Number> counters = new HashMap<String, Number>();
counters.put(counterFieldName, Integer.valueOf(oldIntValue + amount));
updateCounters(counters);
}
/**
* Decrements counter field by 1.
*
* @param btr BelongsToRecordRelation
*/
public void decrementCounter(BelongsToRecordRelation btr) {
decrementCounter(((BelongsToRelation)btr.getRelation()).getCounterCacheName());
}
/**
* Decrements counter field by 1.
*
* @param counterFieldName
*/
public void decrementCounter(String counterFieldName) {
decrementCounter(counterFieldName, +1);
}
/**
* Decrements counter field by 1.
*
* @param btr BelongsToRecordRelation
* @param amount
*/
public void decrementCounter(BelongsToRecordRelation btr, int amount) {
decrementCounter(((BelongsToRelation)btr.getRelation()).getCounterCacheName(), amount);
}
/**
* Decrements counter field by amount.
*
* @param counterFieldName
* @param amount
*/
public void decrementCounter(String counterFieldName, int amount) {
Object oldCount = getField(counterFieldName);
int oldIntValue = Util.getSafeIntValue(oldCount);
Map<String, Number> counters = new HashMap<String, Number>();
counters.put(counterFieldName, Integer.valueOf(oldIntValue - amount));
updateCounters(counters);
}
/**
* <p>Updates the record.</p>
*
* <p>This method is the same as {@link #update(boolean) update(true)}.</p>
*/
public int update() {
return update(true);
}
/**
* <p>
* Updates the record. If <tt>changedOnly</tt> is <tt>true</tt>, only
* changed fields are included in SQL query.
* </p>
*
* <p>
* If there is no primary key defined for the model, data from all columns
* will be used as update conditions.
* </p>
*
* <p>
* This method calls {@link #beforeUpdate() beforeUpdate} before the real
* update, and {@link #afterUpdate() afterUpdate} after the update
* execution.
* </p>
*
* @param changedOnly
* true if only changed fields are included in SQL query
* @return number of records updated
*/
public int update(boolean changedOnly) {
if (isFreezed()) throw new InvalidOperationException(this, "update", "freezed");
ImplicitTransactionManager tm = TransactionManagerUtil.getImplicitTransactionManager();
int updateCount = -1;
try {
tm.beginTransactionImplicit();
beforeUpdate();
updateCount = internal_update(changedOnly);
if (updateCount > 1)
log.warn("Should only update one, but actually updated "
+ updateCount + " records.");
afterUpdate();
tm.commitTransactionImplicit();
ActiveRecordUtil.getGateway(getClass()).getModelCacheClient().clearCache("update");
}
catch(BaseSQLException bdex) {
tm.rollbackTransactionImplicit();
throw bdex;
}
finally {
tm.releaseResourcesImplicit();
}
return updateCount;
}
/**
* <p>Deletes the record based on the current primary key values.</p>
*
* <p>If there is no primary key defined for the model, data from all
* columns will be used as delete conditions.</p>
*
* @return int number of records deleted
*/
public int delete() {
if (isFreezed() || isNewRecord()) return -1;
ImplicitTransactionManager tm = TransactionManagerUtil.getImplicitTransactionManager();
int count = -1;
try {
tm.beginTransactionImplicit();
beforeDelete();
before_internal_delete();
//populate a Map with primary key values
String[] pkNames = rowInfo.getPrimaryKeyColumnNames();
if (pkNames == null || pkNames.length == 0) pkNames = rowInfo.getColumnNames();
int size = pkNames.length;
Map<String, Object> inputs = new HashMap<String, Object>();
for (int i=0; i< size; i++) {
String columnName = pkNames[i];
Object columnData = rowData.getField(columnName);
inputs.put(columnName, columnData);
}
count = ActiveRecordUtil.getGateway(getClass()).deleteAll(inputs);
after_internal_delete();
afterDelete();
freeze();
tm.commitTransactionImplicit();
}
catch(BaseSQLException bdex) {
tm.rollbackTransactionImplicit();
throw bdex;
}
finally {
tm.releaseResourcesImplicit();
}
return count;
}
/**
* Reloads the current record based on its primary key values.
*/
public void reload() {
if (isFreezed()) throw new InvalidOperationException(this, "reload", "freezed");
beforeFind();
find();
afterFind();
}
/**
* <p>Saves the current record.</p>
*
* <p>The SQL query includes only those fields that are changed.</p>
*
* <p>This method is the same as {@link #save(boolean) save(true)}.</p>
*/
public void save() {
save(true);
}
/**
* <p>
* Saves the current record.
* </p>
*
* <p>
* If <tt>changedOnly</tt> is <tt>true</tt>, the SQL query includes only
* those fields that are changed; otherwise, includes all fields.
* </p>
*
* <p>
* If the record exists , this method uses {@link #update(boolean)
* update(boolean)}, otherwise {@link #create(boolean) create(true)}.
* </p>
*
* <p>
* This method calls {@link #beforeSave() beforeSave} before the real save,
* and {@link #afterSave() afterSave()} after the save execution.
* </p>
*
* @param changedOnly
* true if only changed fields are included in SQL query
*/
public void save(boolean changedOnly) {
if (isFreezed())
throw new InvalidOperationException(this, "saveChanged", "freezed");
beforeSave();
if (isNewRecord())
create(changedOnly);
else
update(changedOnly);
afterSave();
}
/**
* <p>Saves the current record and then reloads from database.</p>
*
* <p>This method is useful when other process such as database trigger
* may change the record.</p>
*/
public void saveAndReload() {
if (isFreezed()) throw new InvalidOperationException(this, "SaveAndReload", "freezed");
save();
reload();
}
/**
* <p>Sets column values by parsing input string nameValuePairs.</p>
*
* <p>String nameValuePairs has the following format for example:
* <pre>
* firstName=John, lastName=Doe, age=10,...
* or firstName=John|lastName=Doe|age=10|...
* or firstName=John&lastName=Doe&age=10&...
* </pre>
* You can use either ',', or '|' or '&' to separate each condition.
* </p>
*
* <p>The name of the data entry in the nameValuePairs is corresponding
* to a column name in the RowInfo object. If the name is not a column name,
* its value is ignored. If the column name is not in the names of
* the nameValuePairs, the column data is set to null.</p>
*
* <p>If a column name is protected, its data is unaffected. Use setData
* method to set its data.</p>
*/
public void clearAndSetData(String nameValuePairs) {
clearAndSetData(Converters.convertStringToMap(nameValuePairs));
}
/**
* <p>Sets column values by parsing input string nameValuePairs
* from a Map.</p>
*
* <p>The key of the data entry in the Map is corresponding to a
* column name in the RowInfo object. If the key is not a column name,
* its value is ignored. If the column name is not in the key set of
* the Map, the column data is set to null.</p>
*
* <p>If a column name is protected, its data is unaffected. Use setData
* method to set its data.</p>
*/
public void clearAndSetData(Map<String, ?> inputDataMap) {
if (isFreezed()) throw new InvalidOperationException(this, "clearAndSetData", "freezed");
//filter out protected fields before setting data
rowData.clearAndSetData(filterProtectedFields(inputDataMap));
}
/**
* <p>Returns the record data as map. The keys in the map are field names
* in upper case. An empty map is returned if the underline
* data is not retrieved or the record is new.</p>
*
* @return map of record data
*/
public Map<String, Object> data() {
Map<String, Object> dataMap = new HashMap<String, Object>();
dataMap.putAll(extraFieldsMap);
if (rowData != null) {
dataMap.putAll(rowData.getDataMap());
}
return dataMap;
}
/**
* <p>Sets column values by parsing input string nameValuePairs
*
* <p>String nameValuePairs has the following format for example:
* <pre>
* firstName=John, lastName=Doe, age=10,...
* or firstName=John|lastName=Doe|age=10|...
* or firstName=John&lastName=Doe&age=10&...
* </pre>
* You can use either ',', or '|' or '&' to separate each condition.</p>
*
* <p>The name of the data entry in the nameValuePairs is corresponding to
* a column name in the RowInfo object. If the name is not a column name,
* its value is ignored. If the column name is not in the names of the
* nameValuePairs, the column data is not updated. To set those column data
* to null when the column name is not in the key set, use the
* {@link #clearAndSetData(java.lang.String) clearAndSetData} method.</p>
*
* <p>This method does not save data to database. Use save() or create() or
* update() to save data to database.</p>
*
* <p>If a column name is protected, its data is unaffected. Use setData
* method to set its data.</p>
*/
public void setData(String nameValuePairs) {
if (isFreezed()) throw new InvalidOperationException(this, "setData", "freezed");
if (nameValuePairs == null || "".equals(nameValuePairs.trim())) return;
setData(Converters.convertStringToMap(nameValuePairs));
}
/**
* <p>Sets column values by parsing input string nameValuePairs from a Map.</p>
*
* <p>The key of the data entry in the Map is corresponding to a
* column name in the RowInfo object. If the key is not a column name, its
* value is ignored. If the column name is not in the key set of the
* Map, or the column is readonly or not writable, or is primary key of an
* existing record, the column data is not updated.</p>
*
* <p>To set those column data to null when the column name is not in the key
* set, use the
* {@link #clearAndSetData(java.util.Map) clearAndSetData} method.</p>
*
* <p>This method does not save data to database. Use save() or create() or
* update() to save data to database.</p>
*
* <p>If a column name is protected, its data is unaffected.</p>
*/
public void setData(Map<String, ?> inputDataMap) {
if (isFreezed()) throw new InvalidOperationException(this, "setData", "freezed");
if (inputDataMap == null || inputDataMap.size() == 0) return;
beforeSetData();
Map<String, Object> data = new HashMap<String, Object>(inputDataMap.size());
for (Map.Entry<String, ?> entry : inputDataMap.entrySet()) {
String key = entry.getKey();
if (rowInfo.isPrimaryKeyColumn(key) && existInDatabase) continue;
data.put(key, entry.getValue());
}
List<String> modifiedFieldNames = setExtraFieldData(data);
//filter out protected fields before setting data
List<String> modifiedColumnNames = rowData.setData(filterProtectedFields(data));
List<String> modifiedNames = new ArrayList<String>();
if (modifiedFieldNames != null && modifiedFieldNames.size() > 0)
modifiedNames.addAll(modifiedFieldNames);
if (modifiedColumnNames != null && modifiedColumnNames.size() > 0)
modifiedNames.addAll(modifiedColumnNames);
afterSetData(modifiedNames);
}
/**
* <p>Sets column data for a column index.</p>
*
* <p>Index starts from 0: 0, 1, 2, ...</p>
*
* <p>This method does not save data to database. Use save() or create()
* or update() to save data to database.</p>
*/
public void setData(int index, Object columnData) {
if (isFreezed()) throw new InvalidOperationException(this, "setData", "freezed");
beforeSetData();
rowData.setField(index, columnData);
afterSetData(index);
}
/**
* <p>Sets data for a field.</p>
*
* <p>If there is no such a field, an InvalidColumnNameException
* will be thrown.</p>
*
* <p>This method does not save data to database. Use save() or create() or
* update() to save data to database.</p>
*/
public void setData(String fieldName, Object fieldData) {
if (isFreezed()) throw new InvalidOperationException(this, "setData", "freezed");
beforeSetData();
if (isExtraField(fieldName)) {
setExtraFieldData(fieldName, fieldData);
}
else if (!isProtectedField(fieldName)) {
rowData.setField(fieldName, fieldData);
}
afterSetData(fieldName);
}
/**
* Checks if a data map contains primary key.
*
* @param data Map of input data
* @return boolean state indicates if the data map contains primary field(s)
*/
public boolean containsPrimaryKey(Map<String, Object> data) {
boolean state = false;
if (data != null && data.size() > 0) {
for (String key : data.keySet()) {
if (rowInfo.isPrimaryKeyColumn(key)) {
state = true;
break;
}
}
}
return state;
}
/**
* Returns true if the record has primary key.
*/
public boolean hasPrimaryKey() {
return rowInfo.hasPrimaryKey();
}
/**
* Returns a string array of primary key names.
*
* @return String[]
*/
public String[] getPrimaryKeyNames() {
return rowInfo.getPrimaryKeyColumnNames();
}
/**
* Returns the data map for primary keys. The keys in the map are primary
* key column names in lower case. An empty map is returned if the underline
* data is not retrieved or the record is new.
*
* @return map of primary key data
*/
public Map<String, Object> getPrimaryKeyDataMap() {
return (rowData != null)?rowData.getPrimaryKeyDataMap():new HashMap<String, Object>();
}
/**
* Returns an AssociatedRecord instance of a specific class type.
*
* @param target class of the associated record
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(Class<? extends ActiveRecord> target) {
return associated(target, false);
}
/**
* Returns an AssociatedRecord instance of a specific class type.
*
* @param target class of the associated record
* @param refresh true if reload database data
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(Class<? extends ActiveRecord> target, boolean refresh) {
return associated(target, null, refresh);
}
/**
* <p>Returns an AssociatedRecord instance of a specific class type.</p>
*
* <p>See top of this class for <tt>options</tt> examples.</p>
*
* @param target class of the associated record
* @param options A string of options.
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(Class<? extends ActiveRecord> target, String options) {
return associated(target, options, false);
}
/**
* <p>Returns an AssociatedRecord instance of a specific class type.</p>
*
* <p>See top of this class for <tt>options</tt> examples.</p>
*
* @param target class of the associated record
* @param options A string of options.
* @param refresh true if reload database data
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(Class<? extends ActiveRecord> target, String options, boolean refresh) {
return associated(ActiveRecordUtil.getModelName(target), options, refresh);
}
/**
* <p>Returns an AssociatedRecord instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>belongs-to</tt> or
* <tt>has-one</tt> relation defined in the class.</p>
*
* @param associationId association id
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(String associationId) {
return associated(associationId, false);
}
/**
* <p>Returns an AssociatedRecord instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>belongs-to</tt> or
* <tt>has-one</tt> relation defined in the class.</p>
*
* @param associationId association id
* @param refresh true if reload database data
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(String associationId, boolean refresh) {
return associated(associationId, null, refresh);
}
/**
* <p>Returns an AssociatedRecord instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>belongs-to</tt> or
* <tt>has-one</tt> relation defined in the class.</p>
*
* <p>See top of this class for <tt>options</tt> examples.</p>
*
* @param associationId association id
* @param options A string of options.
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(String associationId, String options) {
return associated(associationId, options, false);
}
/**
* <p>Returns an AssociatedRecord instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>belongs-to</tt> or
* <tt>has-one</tt> relation defined in the class.</p>
*
* <p>See top of this class for <tt>options</tt> examples.</p>
*
* @param associationId association id
* @param options A string of options.
* @param refresh true if reload database data
* @return the AssociatedRecord instance
*/
public AssociatedRecord associated(String associationId, String options, boolean refresh) {
return getRecordRelation(associationId).associatedRecord(options, refresh);
}
/**
* Returns an AssociatedRecords instance of a specific class type.
*
* @param target class of the associated records
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(Class<? extends ActiveRecord> target) {
return allAssociated(target, false);
}
/**
* Returns an AssociatedRecords instance of a specific class type.
*
* @param target class of the associated records
* @param refresh true if reload database data
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(Class<? extends ActiveRecord> target, boolean refresh) {
return allAssociated(target, null, refresh);
}
/**
* <p>Returns an AssociatedRecords instance of a specific class type.</p>
*
* <p>See top of this class for <tt>options</tt> examples.</p>
*
* @param target class of the associated records
* @param options A string of options.
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(Class<? extends ActiveRecord> target, String options) {
return allAssociated(target, null, false);
}
/**
* Returns an AssociatedRecords instance of a specific class type.
*
* @param target class of the associated records
* @param options A string of options.
* @param refresh true if reload database data
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(Class<? extends ActiveRecord> target, String options, boolean refresh) {
String model = ActiveRecordUtil.getModelName(target);
return allAssociated(WordUtil.pluralize(model), options, refresh);
}
/**
* <p>Returns an AssociatedRecords instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>has-many</tt> or
* <tt>has-many-through</tt> relation defined in the class.</p>
*
* @param associationId association id
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(String associationId) {
return allAssociated(associationId, false);
}
/**
* <p>Returns an AssociatedRecords instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>has-many</tt> or
* <tt>has-many-through</tt> relation defined in the class.</p>
*
* @param associationId association id
* @param refresh true if reload database data
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(String associationId, boolean refresh) {
return allAssociated(associationId, null, refresh);
}
/**
* <p>Returns an AssociatedRecords instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>has-many</tt> or
* <tt>has-many-through</tt> relation defined in the class.</p>
*
* <p>See top of this class for <tt>options</tt> examples.</p>
*
* @param associationId association id
* @param options A string of options.
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(String associationId, String options) {
return allAssociated(associationId, options, false);
}
/**
* <p>Returns an AssociatedRecords instance.</p>
*
* <p>The <tt>associationId</tt> is the name of the <tt>has-many</tt> or
* <tt>has-many-through</tt> relation defined in the class.</p>
*
* <p>See top of this class for <tt>options</tt> examples.</p>
*
* @param associationId association id
* @param options A string of options.
* @param refresh true if reload database data
* @return the AssociatedRecords instance
*/
public AssociatedRecords allAssociated(String associationId, String options, boolean refresh) {
return getRecordRelation(associationId).allAssociatedRecords(options, refresh);
}
/**
* Returns an AssociatedRecordsInCategory instance.
*
* @param category name of the category
* @return the AssociatedRecordsInCategory instance
*/
public AssociatedRecordsInCategory allAssociatedInCategory(String category) {
return allAssociatedInCategory(category, null);
}
/**
* Returns an AssociatedRecordsInCategory instance.
*
* @param category name of the category
* @param refresh true if reload database data
* @return the AssociatedRecordsInCategory instance
*/
public AssociatedRecordsInCategory allAssociatedInCategory(String category, boolean refresh) {
return allAssociatedInCategory(category, null, refresh);
}
/**
* Returns an AssociatedRecordsInCategory instance.
*
* @param category name of the category
* @param type type name in the category
* @return the AssociatedRecordsInCategory instance
*/
public AssociatedRecordsInCategory allAssociatedInCategory(String category, String type) {
return allAssociatedInCategory(category, type, false);
}
/**
* Returns an AssociatedRecordsInCategory instance.
*
* @param category name of the category
* @param type type name in the category
* @param refresh true if reload database data
* @return the AssociatedRecordsInCategory instance
*/
public AssociatedRecordsInCategory allAssociatedInCategory(String category, String type, boolean refresh) {
RelationManager.getInstance().registerRelations(getClass());
return new AssociatedRecordsInCategory(this, category, type, refresh);
}
/**
* Returns associated record of this join model based on the type value of
* the type field. For example, tagging.associatedInCategory("taggable").
*
* @param category name of the category
* @return an associated record for the category
*/
public AssociatedRecord associatedInCategory(String category) {
return associatedInCategory(category, false);
}
/**
* Returns associated record of this join model based on the type value of
* the type field. For example, tagging.associatedInCategory("taggable", true).
*
* @param category name of the category
* @param refresh true if reload database data
* @return an associated record for the category
*/
public AssociatedRecord associatedInCategory(String category, boolean refresh) {
Category categoryInstance = RelationManager.getInstance().getCategory(category);
if (categoryInstance == null) {
throw new UnregisteredCategoryException(category);
}
String typeField = categoryInstance.getTypeField();
String typeValue = (String)getField(typeField);
String model = categoryInstance.getEntityByType(typeValue);
return associated(model, refresh);
}
/**
* Check if this record is a dependent of a specific record type
* through primary key. The following must be satisfied:
*
* <ol>
* <li>There must be a belongsTo relation between this record and the parent model type.</li>
* <li>The child record has a primary key and part or whole of that key is the primary key of the parent model type.</li>
* </ol>
*
* For example, EMPLOYEE (pk=id) and DEPENDENT (pk=id, emp_id);
*
* @param parentClz A potential parent model type
* @return true if it is a true parent.
*/
public boolean isPKDependentOf(Class<? extends ActiveRecord> parentClz) {
//condition #1
Relation r = RelationManager.getInstance().getBelongsToRelationBetween(this.getClass(), parentClz);
if (r == null) {
return false;
}
//condition #2
String[] cpk = getPrimaryKeyNames();
if (cpk == null) return false;
String[] left = r.getLeftSideMappingItems();
for (int i = 0; i < left.length; i++) {
String leftKey = left[i];
if (!StringUtil.isStringInArray(leftKey, cpk, true)) return false;
}
return true;
}
/**
* Check if this record is a dependent of a specific record. To be a
* dependent of a specific record type, the following must be satisfied:
*
* <ol>
* <li>There must be a belongsTo relation between this record and the parent model type.</li>
* <li>The cascade property of the parent model must be cascade delete or</li>
* <li>The child record has a primary key and part or whole of the key is the primary key of the parent model.</li>
* <li>The record's foreign key must hold parent record's primary key value.</li>
* </ol>
* For example, EMPLOYEE (pk=id) and DEPENDENT (pk=id, emp_id);
*
* @param parent A potential parent model
* @return true if it is a true parent.
*/
public boolean isDependentOf(ActiveRecord parent) {
if (parent == null || parent.isNewRecord()) return false;
boolean status = true;
//condition #1
Relation r = RelationManager.getInstance().getBelongsToRelationBetween(this.getClass(), parent.getClass());
if (r == null) {
return false;
}
//condition #2
Relation reverseRelation = RelationManager.getInstance().getHasManyRelationBetween(parent.getClass(), this.getClass());
if (reverseRelation != null) {
if (!reverseRelation.allowCascadeDelete()) {
return false;
}
}
else {
reverseRelation = RelationManager.getInstance().getHasOneRelationBetween(parent.getClass(), this.getClass());
if (reverseRelation != null) {
if (!reverseRelation.allowCascadeDelete()) {
return false;
}
}
}
//condition #3
String[] cpk = getPrimaryKeyNames();
if (cpk == null) return false;
String[] left = r.getLeftSideMappingItems();
for (int i = 0; i < left.length; i++) {
String leftKey = left[i];
if (!StringUtil.isStringInArray(leftKey, cpk, true)) return false;
}
//condition #4
if (status) {
String[] fkColumns = r.getLeftSideMappingItems();
String[] parentPKColumns = r.getRightSideMappingItems();
if (fkColumns.length == parentPKColumns.length) {
int size = fkColumns.length;
for (int i = 0; i < size; i++) {
Object fkData = getField(fkColumns[i]);
Object pkData = parent.getField(parentPKColumns[i]);
if (!(fkData != null && pkData != null && fkData.toString().equalsIgnoreCase(pkData.toString()))) {
status = false;
break;
}
}
}
}
return status;
}
/**
* Checks if an instance belongs to another record.
*
* To belong to a record, the following conditions must be satisfied:
* <ol>
* <li>There must be a belongsTo relation between this record and the potential parent record.</li>
* <li>The record's foreign key must hold parent record's primary key value.</li>
* </ol>
*
* @param parent
* @return true if the instance is a child of another record
*/
public boolean isChildOf(ActiveRecord parent) {
if (parent == null || parent.isNewRecord()) return false;
//condition #1
Relation r = RelationManager.getInstance().getBelongsToRelationBetween(this.getClass(), parent.getClass());
if (r == null) {
return false;
}
//condition #2
boolean status = true;
if (status) {
String[] fkColumns = r.getLeftSideMappingItems();
String[] parentPKColumns = r.getRightSideMappingItems();
if (fkColumns.length == parentPKColumns.length) {
int size = fkColumns.length;
for (int i=0; i<size; i++) {
Object fkData = getField(fkColumns[i]);
Object pkData = parent.getField(parentPKColumns[i]);
if (!(fkData != null && pkData != null && fkData.toString().equalsIgnoreCase(pkData.toString()))) {
status = false;
break;
}
}
}
}
return status;
}
/**
* Subclass need to override this method by calling proper relationship
* setup methods: hasOne, belongsTo, hasMany, hasManyThrough, etc.
*/
public void registerRelations() {
;
}
/**
* <p>Sets belongs-to relation.</p>
*
* <p>The association id is the model name of the target class.</p>
*
* @param target the class that is associated with.
*/
public void belongsTo(Class<? extends ActiveRecord> target) {
String model = ActiveRecordUtil.getModelName(target);
belongsTo(target, ActiveRecordConstants.key_model + ":" + model);
}
/**
* <p>Sets belongs-to relation with specified properties.</p>
*
* <p>The association id of the relation is the model name of the target class.</p>
*
* @param target the class that is associated with.
* @param properties string of association properties.
*/
public void belongsTo(Class<? extends ActiveRecord> target, String properties) {
String model = ActiveRecordUtil.getModelName(target);
RelationManager.getInstance().setupRelation(getClass(), Relation.BELONGS_TO_TYPE, model, target, properties);
}
/**
* Sets belongs-to relation.
*
* @param target model name of the associated class.
*/
public void belongsTo(String target) {
belongsTo(target, ActiveRecordConstants.key_model + ":" + target);
}
/**
* Sets belongs-to relation with specified properties.
*
* <p>
* <tt>target</tt> parameter can be either the
* model name of the target or a descriptive string of the target. In
* the latter case, the <tt>properties</tt> parameter must contain key
* <tt>model</tt> to indicate the model name of the target unless it can be
* derived from the target name.
*
* <pre>
* Example:
* target properties
* ------ ----------
* friend model:person
* </pre>
* </p>
*
* <p>
* Example property string:
* In a property string, each name-value pair is separated by ';'
* character, while within each name-value pair, name and value strings
* are separated by ':' character.
*
* For example, a property string like the following
* <blockquote><pre>
* mapping: order_id=id;
* conditions_sql: id in (1, 2, 3); include: category, user;
* order_by: first_name, salary desc; cascade: delete
* </pre></blockquote>
*
* will be converted to a HashMap with the following entries:
* <blockquote><pre>
* key value
* ------------- ---------------
* mapping => order_id=id
* conditions_sql => id in (1, 2, 3)
* include => category, user
* order_by => first_name, salary desc
* cascade => delete
* </pre></blockquote>
* For a complete list of properties, see top of the class or developer guide.
* </p>
*
* @param target target name of the associated class.
* @param properties association properties.
*/
public void belongsTo(String target, String properties) {
RelationManager.getInstance().setupRelation(getClass(), Relation.BELONGS_TO_TYPE, target, null, properties);
}
/**
* <p>Sets up a category with default id field and type field.</p>
*
* <p>This method assumes that the id field is ${category}_id and the type
* field is ${category}_type.</p>
*
* @param category name of the category
*/
public void belongsToCategory(String category) {
String idField = category + "_id";
String typeField = category + "_type";
belongsToCategory(category, idField, typeField);
}
/**
* Sets up a category.
*
* @param idField name of the id column of the category center table
* @param typeField name of the type column of the category center table
* @param category name of the category
*/
public void belongsToCategory(String category, String idField, String typeField) {
RelationManager.getInstance().registerCategory(getClass(), category, idField, typeField);
}
/**
* Sets has-one relation
*
* @param target the class that is associated with.
*/
public void hasOne(Class<? extends ActiveRecord> target) {
String model = ActiveRecordUtil.getModelName(target);
hasOne(target, ActiveRecordConstants.key_model + ":" + model);
}
/**
* <p>Sets has-one relation with specified properties.</p>
*
* <p>The association id of the relation is the model name of the target class.</p>
*
* @param target the class that is associated with.
* @param properties string of association properties.
*/
public void hasOne(Class<? extends ActiveRecord> target, String properties) {
String model = ActiveRecordUtil.getModelName(target);
RelationManager.getInstance().setupRelation(getClass(), Relation.HAS_ONE_TYPE, model, target, properties);
}
/**
* Sets has-one relation.
*
* @param targetModelName model name of the associated class.
*/
public void hasOne(String targetModelName) {
hasOne(targetModelName, ActiveRecordConstants.key_model + ":" + targetModelName);
}
/**
* Sets has-one relation with specified properties.
*
* <p>
* <tt>target</tt> parameter can be either the
* model name of the target or a descriptive string of the target. In
* the latter case, the <tt>properties</tt> parameter must contain
* key <tt>model</tt> to indicate the model name of the target.
*
* <pre>
* Example:
* target properties
* ------ ----------
* setting model:profile
* </pre>
* </p>
*
* <p>
* Example property string:
* In a property string, each name-value pair is separated by ';'
* character, while within each name-value pair, name and value strings
* are separated by ':' character.
*
* For example, a property string like the following
* <blockquote><pre>
* mapping: id=order_id;
* conditions_sql: id in (1, 2, 3); include: category, user;
* order_by: first_name, salary desc; cascade: delete
* </pre></blockquote>
*
* will be converted to a HashMap with the following entries:
* <blockquote><pre>
* key value
* ------------- ---------------
* mapping => id=order_id
* conditions_sql => id in (1, 2, 3)
* include => category, user
* order_by => first_name, salary desc
* cascade => delete
* </pre></blockquote>
* For a complete list of properties, see top of the class or developer guide.
* </p>
*
* @param target target name of the associated class.
* @param properties association properties.
*/
public void hasOne(String target, String properties) {
RelationManager.getInstance().setupRelation(getClass(), Relation.HAS_ONE_TYPE, target, null, properties);
}
/**
* Sets has-many relation
*
* @param target the class that is associated with.
*/
public void hasMany(Class<? extends ActiveRecord> target) {
String model = ActiveRecordUtil.getModelName(target);
hasMany(target, ActiveRecordConstants.key_model + ":" + model);
}
/**
* <p>Sets has-many relation with specified properties.</p>
*
* <p>The association id of the relation is the plural form of the model name of the target class.</p>
*
* @param target the class that is associated with.
* @param properties string of association properties.
*/
public void hasMany(Class<? extends ActiveRecord> target, String properties) {
String model = ActiveRecordUtil.getModelName(target);
RelationManager.getInstance().setupRelation(getClass(), Relation.HAS_MANY_TYPE, WordUtil.pluralize(model), target, properties);
}
/**
* Sets has-many relation.
*
* @param targets plural form of a model name of the associated class.
*/
public void hasMany(String targets) {
String targetModel = WordUtil.singularize(targets);
hasMany(targets, ActiveRecordConstants.key_model + ":" + targetModel);
}
/**
* Sets has-many relation with specified properties.
*
* <p>
* <tt>targets</tt> parameter can be either a
* plural form of the model name of the target or a descriptive string
* of the target. In the latter case, the <tt>properties</tt> parameter
* must contain key <tt>model</tt> to indicate the model name of the target.
*
* <pre>
* Example:
* targets properties
* ------- ----------
* people model:person
* </pre>
* </p>
*
* <p>
* Example property string:
* In a property string, each name-value pair is separated by ';'
* character, while within each name-value pair, name and value strings
* are separated by ':' character.
*
* For example, a property string like the following
* <blockquote><pre>
* mapping: id=order_id;
* conditions_sql: id in (1, 2, 3); include: category, user;
* order_by: first_name, salary desc; cascade: delete
* </pre></blockquote>
*
* will be converted to a HashMap with the following entries:
* <blockquote><pre>
* key value
* ------------- ---------------
* mapping => id=order_id
* conditions_sql => id in (1, 2, 3)
* include => category, user
* order_by => first_name, salary desc
* cascade => delete
* </pre></blockquote>
* For a complete list of properties, see top of the class or developer guide.
* </p>
*
* @param targets plural form of target name of the associated class.
* @param properties association properties.
*/
public void hasMany(String targets, String properties) {
RelationManager.getInstance().setupRelation(getClass(), Relation.HAS_MANY_TYPE, targets, null, properties);
}
/**
* <p>Sets has-many-through relation.</p>
*
* <p>
* This is equivalent to {@link #hasManyThrough(java.lang.String, java.lang.String)}
* method with the plural form of the model name as the association name.
* </p>
*
* @param target target class.
* @param through middleC class.
*/
public void hasManyThrough(Class<? extends ActiveRecord> target, Class<? extends ActiveRecord> through) {
hasManyThrough(target, through, null);
}
/**
* <p>Sets has-many-through relation.</p>
*
* <p>
* This is equivalent to {@link #hasManyThrough(java.lang.String, java.lang.String, java.lang.String)}
* method with the plural form of the model name as the association name.
* </p>
*
* @param target target class.
* @param through middleC class.
* @param properties properties string.
*/
public void hasManyThrough(Class<? extends ActiveRecord> target, Class<? extends ActiveRecord> through, String properties) {
hasManyThrough(target, through, properties, null);
}
/**
* <p>Sets has-many-through relation.</p>
*
* <p>
* This is equivalent to {@link #hasManyThrough(java.lang.String, java.lang.String, java.lang.String, java.util.Map)}
* method with the plural form of the model name as the association name.
* </p>
*
* @param target target class.
* @param through middleC class.
* @param properties properties string.
* @param joinInputs data map for the join table.
*/
public void hasManyThrough(Class<? extends ActiveRecord> target, Class <? extends ActiveRecord>through, String properties, Map<String, Object> joinInputs) {
String targetModel = ActiveRecordUtil.getModelName(target);
String targets = WordUtil.pluralize(targetModel);
String throughModel = ActiveRecordUtil.getModelName(through);
String throughs = WordUtil.pluralize(throughModel);
RelationManager.getInstance().setupHasManyThroughRelation(getClass(), targets, throughs, properties, joinInputs);
}
/**
* <p>Sets has-many-through relation.</p>
*
* <p>
* There are two pre-requisits for setting up a has-many-through relation:
* <ul>
* <li>The owner class must have a has-many relation named
* <tt>throughAssociationId</tt> with the middleC class.</li>
* <li>The middleC class must have a either a belongs-to or a has-many
* relation named <tt>targets</tt> with the target class.</li>
* </ul>
* </p>
*
* @param targets plural form of target name of the associated class.
* @param throughAssociationId the name of the association that is in the middle.
*/
public void hasManyThrough(String targets, String throughAssociationId) {
hasManyThrough(targets, throughAssociationId, null);
}
/**
* <p>Sets has-many-through relation with specified properties.</p>
*
* <p>
* There are two pre-requisits for setting up a has-many-through relation:
* <ul>
* <li>The owner class must have a has-many relation named
* <tt>throughAssociationId</tt> with the middleC class.</li>
* <li>The middleC class must have a either a belongs-to or a has-many
* relation named <tt>targets</tt> with the target class.</li>
* </ul>
* </p>
*
* <p>Example property string:
* In a property string, each name-value pair is separated by ';'
* character, while within each name-value pair, name and value strings
* are separated by ':' character.</p>
*
* <p>For example, a property string like the following
* <blockquote><pre>
* conditions_sql: id in (1, 2, 3); include: category, user;
* order_by: first_name, salary desc; cascade: delete
* </pre></blockquote>
*
* will be converted to a HashMap with the following entries:
* <blockquote><pre>
* key value
* ------------- ---------------
* conditions_sql => id in (1, 2, 3)
* include => category, user
* order_by => first_name, salary desc
* cascade => delete
* </pre></blockquote>
* For a complete list of properties, see top of the class or developer guide.</p>
*
* @param targets plural form of target name of the associated class.
* @param throughAssociationId the name of the association that is in the middle.
* @param properties properties string.
*/
public void hasManyThrough(String targets, String throughAssociationId, String properties) {
hasManyThrough(targets, throughAssociationId, properties, null);
}
/**
* <p>Sets has-many-through relation with specified properties and join
* through table data.</p>
*
* <p>
* There are two pre-requisits for setting up a has-many-through relation:
* <ul>
* <li>The owner class must have a has-many relation named
* <tt>throughAssociationId</tt> with the middleC class.</li>
* <li>The middleC class must have a either a belongs-to or a has-many
* relation named <tt>targets</tt> with the target class.</li>
* </ul>
* </p>
*
* <p>See description of {@link #hasManyThrough(java.lang.String, java.lang.String, java.lang.String)}
* method for details about <tt>properties</tt>.</p>
*
* @param targets plural form of target name of the associated class.
* @param throughAssociationId the name of the association that is in the middle.
* @param properties properties string.
* @param joinInputs data map for the join table.
*/
public void hasManyThrough(String targets, String throughAssociationId, String properties, Map<String, Object> joinInputs) {
RelationManager.getInstance().setupHasManyThroughRelation(getClass(), targets, throughAssociationId, properties, joinInputs);
}
/**
* This method adds a bunch of methods in many classes.
* <ol>
* <li> A has-many-through association from owner to each target class.</li>
* <li> A has-many association from each target to through class.</li>
* <li> A has-many-through association from each target to owner class.</li>
* <li> A belongs-to association from through to each target class.</li>
* </ol>
*
* In order to establish the associations, the method assumes the following:
* <ol>
* <li> The type value of the category type column is the model name of
* each corresponding target class.</li>
* <li> The primary key of each target class is "id".</li>
* <li> The mapping string between each target class and through class is
* "id= category's id column".</li>
* <li> The association property from each target to through contains "cascade: delete".</li>
* </ol>
*
* <p>
* If any of the above assumptions are not satisfied, you need to use the
* other <tt>hasManyInCategoryThrough </tt> method which gives you more
* control on specifying the associations.
* </p>
*
* <p>Example usage: </p>
* <p>Assuming there are image files and text files in a folder. We create
* three models: images, texts, folders. We also use linkings model to
* link folders with images and texts files. We will create the following
* classes:</p>
*
* <pre>
* CREATE TABLE linkings (
* id INTEGER AUTO_INCREMENT,
* folder_id INTEGER,
* linkable_id INTEGER,
* linkable_type VARCHAR(20),
* PRIMARY KEY(id)
* )
*
* class Linking extends ActiveRecord {
* public void registerRelations() {
* belongsTo(Folder.class);
* belongsToCategory("linkable");
* }
* }
*
* class Folder extends ActiveRecord {
* public void registerRelations() {
* hasMany(Linking.class);
* hasManyInCategoryThrough(Folder.class,
* new Class[]{Image.class, Text.class},
* "linkable", Linking.class);
* }
* }
*
* class Image extends ActiveRecord {
* }
*
* class Text extends ActiveRecord {
* }
* </pre>
*
* The following codes show how to get total of ownership for a customer:
* <pre>
* //Find all ownerships of a customer
* ActiveRecord customerHome = ActiveRecordUtil.getHomeInstance(Customer.class);
* ActiveRecord customer = customerHome.find("id=1");
* int total = customer.allAssociatedInCategory("ownerable").size();
* </pre>
*
* It is also easy to add a dvd to the ownership of the customer:
* <pre>
* //Assign a dvd to a customer
* ActiveRecord dvdHome = ActiveRecordUtil.getHomeInstance(Dvd.class);
* ActiveRecord dvd = dvdHome.find("id=4");
* List dvds = customer.allAssociatedInCategory("ownerable").add(dvd).getRecords();
* </pre>
*
* @param targets array of target classes
* @param category the category which the targets act as
* @param through the middle join class between owner and targets
*/
public void hasManyInCategoryThrough(Class<? extends ActiveRecord>[] targets,
String category, Class<? extends ActiveRecord> through) {
if (targets == null || targets.length == 0) {
throw new IllegalArgumentException("Target array cannot be empty.");
}
//make sure category center is loaded first
RelationManager.getInstance().registerRelations(through);
Category categoryInstance = RelationManager.getInstance().getCategory(category);
if (categoryInstance == null) {
throw new UnregisteredCategoryException(category);
}
String idField = categoryInstance.getIdField();
String typeField = categoryInstance.getTypeField();
String cTableName = ActiveRecordUtil.getTableName(through);
int targetTotal = targets.length;
String[] abProperties = new String[targetTotal];
String[] types = new String[targetTotal];
String relationType = Relation.HAS_MANY_TYPE;
String[] bcProperties = new String[targetTotal];
@SuppressWarnings("unchecked")
Map<String, Object>[] joinInputs = new HashMap[targetTotal];
String[] cbProperties = new String[targetTotal];
String cbMapping = ActiveRecordConstants.key_mapping + ": " + idField + "=id; ";
for (int i=0; i<targetTotal; i++) {
types[i] = ActiveRecordUtil.getModelName(targets[i]);
String throughTypeCondition = ActiveRecordConstants.key_conditions_sql + ": " + cTableName + "." + typeField + "='" + types[i] + "'";
abProperties[i] = throughTypeCondition;
bcProperties[i] = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " +
throughTypeCondition + "; " + ActiveRecordConstants.key_cascade + ": delete";
Map<String, Object> inputs = new HashMap<String, Object>();
inputs.put(typeField, types[i]);
joinInputs[i] = inputs;
cbProperties[i] = cbMapping;
}
//baProperties are null.
hasManyInCategoryThrough(targets, category, through, joinInputs,
abProperties, types, relationType,
bcProperties, joinInputs, null, null);
}
/**
* This method adds a bunch of methods in many classes.
* <pre>
* <li> A has-many-through association from owner to each target class.</li>
* <li> A has-many association from each target to through class.</li>
* <li> A has-many-through association from each target to owner class.</li>
* <li> A belongs-to association from through to each target class.</li>
* </pre>
*
* Assuming owner class is A, target class is B, through class is C,
* <pre>
* <tt>abProperties</tt> is join properties from A to B,
* <tt>bcProperties</tt> is join properties from B to C,
* <tt>cbProperties</tt> is join properties from C to B,
* <tt>baProperties</tt> is join properties from B to A.
* </pre>
*
* @param targets array of target classes
* @param category the category which the targets act as
* @param through the middle join class between owner and targets
* @param acJoinInputs array of data map for the join through table.
* @param abProperties properties from owner to target class
* @param types array of join types in the category, default to model name
* @param relationType either has-many or has-one
* @param bcProperties array of properties from each target to through class
* @param bcJoinInputs array of data map for the join through table.
* @param cbProperties array of properties from through to each target class
* @param baProperties array of properties from each target to owner class
*/
public void hasManyInCategoryThrough(Class<? extends ActiveRecord>[] targets,
String category, Class<? extends ActiveRecord> through, Map<String, Object>[] acJoinInputs,
String[] abProperties, String[] types, String relationType,
String[] bcProperties, Map<String, Object>[] bcJoinInputs, String[] cbProperties,
String[] baProperties) {
if (targets == null || targets.length == 0) {
throw new IllegalArgumentException("Target array cannot be empty.");
}
//make sure category center is loaded first
RelationManager.getInstance().registerRelations(through);
Category categoryInstance = RelationManager.getInstance().getCategory(category);
if (categoryInstance == null) {
throw new UnregisteredCategoryException(category);
}
String idField = categoryInstance.getIdField();
String typeField = categoryInstance.getTypeField();
String cTableName = ActiveRecordUtil.getTableName(through);
//prepare
int targetTotal = targets.length;
if (abProperties == null) abProperties = new String[targetTotal];
if (bcProperties == null) bcProperties = new String[targetTotal];
if (cbProperties == null) cbProperties = new String[targetTotal];
if (baProperties == null) baProperties = new String[targetTotal];
String cbMappingProperty = ActiveRecordConstants.key_mapping + ": " + idField + "=id; ";
//#1, #2, #4, #3
for (int i=0; i<targetTotal; i++) {
Class<? extends ActiveRecord> target = targets[i];
String targetEntityName = ActiveRecordUtil.getModelName(targets[i]);
String type = "";
if (types != null) type = types[i];
if (type == null) type = targetEntityName;
String throughTypeCondition = ActiveRecordConstants.key_conditions_sql + ": " + cTableName + "." + typeField + "='" + type + "'";
String abProperty = abProperties[i];
if (abProperty == null) {
abProperty = throughTypeCondition;
}
else {
if (abProperty.indexOf(ActiveRecordConstants.key_conditions_sql) == -1) {
abProperty = throughTypeCondition + "; " + abProperty;
}
}
String bcProperty = bcProperties[i];
if (bcProperty == null) {
bcProperty = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " +
throughTypeCondition + "; cascade: delete";
}
else {
if (bcProperty.indexOf(ActiveRecordConstants.key_conditions_sql) == -1) {
bcProperty = throughTypeCondition + "; " + bcProperty;
}
if (bcProperty.indexOf(ActiveRecordConstants.key_mapping) == -1) {
bcProperty = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " + bcProperty;
}
if (bcProperty.indexOf(ActiveRecordConstants.key_cascade) == -1) {
bcProperty = ActiveRecordConstants.key_cascade + ": delete" + "; " + bcProperty;
}
}
Map<String, Object> acJoinInputsMap = acJoinInputs[i];
if (acJoinInputsMap == null) {
acJoinInputsMap = new HashMap<String, Object>();
}
if (acJoinInputsMap.size() == 0) {
acJoinInputsMap.put(typeField, type);
}
String cbProperty = cbProperties[i];
if (cbProperty == null) {
cbProperty = cbMappingProperty;
}
else {
if (cbProperty.indexOf(ActiveRecordConstants.key_mapping) == -1) {
cbProperty = cbMappingProperty + "; " + cbProperty;
}
}
//#2. A has-many association from each target to through class.
//#4. A belongs-to association from through to each target class.
ActiveRecord targetHome = ActiveRecordUtil.getHomeInstance(target);
targetHome.actAsInCategory(type, category,
relationType, through, bcProperty, cbProperty);
//#1. A has-many-through association from owner to each target class.
//Note: need to add a has-many relation between owner and through
// as this is a prerequisit for setting up a has-many-through relation.
if (RelationManager.getInstance().existsHasManyRelationBetween(getClass(), through)) {
hasMany(through);
}
hasManyThrough(target, through, abProperty, acJoinInputsMap);
Map<String, Object> bcJoinInputsMap = bcJoinInputs[i];
if (bcJoinInputsMap == null) {
bcJoinInputsMap = new HashMap<String, Object>();
}
if (bcJoinInputsMap.size() == 0) {
bcJoinInputsMap.put(typeField, type);
}
//#3. A has-many-through association from each target to owner class.
//Note: need to add a belongs-to relation between through and owner
// as this is a prerequisit for setting up a has-many-through relation.
if (RelationManager.getInstance().existsBelongsToRelationBetween(through, getClass())) {
ActiveRecord throughHome = ActiveRecordUtil.getHomeInstance(through);
throughHome.belongsTo(getClass());
}
targetHome.hasManyThrough(getClass(), through, baProperties[i], bcJoinInputsMap);
}
}
/**
* Returns true if the field is a database table field.
*
* @param fieldName the field name to check.
* @return boolean true if the field is a database table field.
*/
public boolean isColumnField(String fieldName) {
boolean column = false;
if (fieldName != null) {
column = rowInfo.isValidColumnName(fieldName);
}
return column;
}
/**
* Returns true if the field is an extra field.
*
* @param fieldName the field name to check.
* @return boolean true if the field is an extra field.
*/
public boolean isExtraField(String fieldName) {
boolean extra = false;
if (fieldName != null) {
extra = extraFields.contains(fieldName.toUpperCase());
}
return extra;
}
/**
* Returns a list of modified field names.
*
* @return list of modified field names
*/
public List<String> getModifiedFields() {
return modifiedColumns;
}
/**
* Checks if a field is changed.
*
* @param fieldName the field name to check
* @return true if the field is changed.
*/
public boolean isFieldChanged(String fieldName) {
verifyExistenceOfField(fieldName);
boolean changed = false;
if (isLegalField(fieldName)) {
changed = modifiedColumns.contains(fieldName.toUpperCase());
}
return changed;
}
/**
* Checks whether a field is a legal field of this model.
*
* @param fieldName the field name to check
* @return true if the field is either a column or an extra field.
*/
public boolean isLegalField(String fieldName) {
boolean legal = true;
if (!isExtraField(fieldName) && !rowInfo.isValidColumnName(fieldName)) {
legal = false;
}
return legal;
}
/**
* Checks if a field name exists in an ActiveRecord class.
*
* @param fieldName a field name
*/
public void verifyExistenceOfField(String fieldName) {
if (!isLegalField(fieldName)) {
throw new GenericException("Field [" + fieldName + "] is not an attribute of class " + getClass().getName() + ".");
}
}
/**
* Returns default database connection name. This name is defined in the
* database properties file.
*
* @return default database connection name
*/
private static String getDefaultConnectionName() {
return DatabaseConfig.getInstance().getDefaultDatabaseConnectionName();
}
/**
* <p>
* Returns a full table name in the database. By default the table name is
* a short class name. Subclass may override this method to provide a more
* meaningful table name. </p>
*
* <p>
* The default table name is a short version of current class name. Java
* class name starts with capital letter. If the class name has more than
* one capital letter, an underscore is added as part of the table name.</p>
*
* <pre>
* Examples:
*
* Class Name: Table Name:
* ---------------------------------------------------
* com.example.model.User users
* com.example.model.LineItem line_items
* com.example.model.UserAccount user_accounts
* com.example.model.UserURL user_urls
* </pre>
*
* @return String table name
*/
public String getTableName() {
if (tableName != null) return tableName;
tableName = DatabaseConfig.getInstance().getFullTableName(getDefaultTableName());
return tableName;
}
/**
* Returns a simple version of the table name.
*
* <p>
* For example, if the table name is "CRM_users_US" which has a prefix
* "CRM_" and a suffix "_US", the returned slim table name is just
* "users". Both the prefix and the suffix are removed in the return value.
* </p>
*
* @return a simple version of table name.
*/
public String getSimpleTableName() {
if (simpleTableName == null) {
simpleTableName = DatabaseConfig.getInstance().getSimpleTableName(tableName);
}
return simpleTableName;
}
/**
* <p>Sets populating rules for primary keys.
*
* <p>Subclass may override this method to use one of the four following
* ways to provide primary key values.
*
* <p>There are four ways to set up the name-rule pair for primary key:
* <pre>
* 1. Do nothing. This is the default. that means the database will
* autogenerate a primary key. This feature is available on MySQL, but
* not on Oracle yet. If you use this feature, that means the primary
* key can not be a composite field.
*
* 2. If the primary key value is a result of SQL query,
* the entry may look like this:
* ("id", "sql=SELECT max(id)+1 FROM employee");
*
* 3. If the primary key value is a result of a named SQL query,
* the entry may look like this:
* ("id", "sqlkey=employee_id_query");
* where the value "employee_id_query" is a named query in
* sql.properties file.
*
* 4. Use a fixed value:
* Map("id", "1000");
* </pre>
*
* @return a data map for primary keys
*/
protected Map<String, Object> getPrimaryKeyRules() {
return null;
}
protected String getDeleteSQL() {
return rowInfo.getDeleteSqlInJDBCStyle();
}
private SqlService getSqlService() {
return SqlServiceConfig.getSqlService();
}
/**
* <p>Data for the protected fields are not affected by massive setData
* method.</p>
*
* <p>Subclass need to override this method by calling setProtectedFields
* method to declare protected fields.</p>
*/
protected void declaresProtectedFields() {
;
}
/**
* <p>Extra fields are not stored in database.</p>
*
* <p>Subclass need to override this method by calling setExtraFields
* method to declare extra fields.</p>
*/
protected void declaresExtraFields() {
;
}
/**
* Initializes the record
*/
private void initialize(String connectionName, String table) {
rowInfo = lookupAndRegister(connectionName, table).getHeader();
rowData = new RowData(rowInfo, null);
dirty = false;//reset dirty as it was set in setData()
existInDatabase = false;
freezed = false;
//load protected fields
declaresProtectedFields();
//load extra fields
declaresExtraFields();
}
/**
* <p>Returns default table name. </p>
*
* <p>The default table name is a short version of current class name. It is
* always in pluralized form unless otherwise indicated by the property
* <tt>use.plural.table.name</tt> in property file. </p>
*
* <p>Java class name starts with capital letter. If the class name has more
* than one capital letter, an underscore is added as part of the table
* name. </p>
*
* <pre>
* Examples:
* Class Name: Table Name:
* ---------------------------------------------------
* com.example.model.User users
* com.example.model.LineItem line_items
* com.example.model.UserAccount user_accounts
* com.example.model.UserURL user_urls
* </pre>
*
* @return String
*/
private String getDefaultTableName() {
String className = Util.getShortClassName(this.getClass());
String tname = className;
if (DatabaseConfig.getInstance().usePluralTableName()) {
tname = WordUtil.tableize(tname);
}
else {
tname = WordUtil.underscore(tname);
}
return tname;
}
/**
* Sets record data when creating an instance of the record
*/
void populateDataFromDatabase(RowData rd) {
if (rd == null) {
rowData.clearData();
}
else {
rowData = rd;
Map<String, Object> pkDataMap = rowData.getPrimaryKeyDataMap();
if (pkDataMap == null || pkDataMap.size() == 0) {
String[] pkNames = getPrimaryKeyNames();
rowData.getRowInfo().setPrimaryKeyColumns(pkNames);
pkDataMap = rowData.getPrimaryKeyDataMap();
}
existInDatabase = true;
}
}
/**
* Returns table meta data. <tt>table</tt> is a full table name.
*/
private TableInfo lookupAndRegister(String connName, String table) {
TableInfo ti = SqlExpressUtil.lookupTableInfo(connName, table);
if (ti == null) {
throw new IllegalArgumentException("Failed to look up table '" +
table + "' for connection '" + connName + "'.");
}
return ti;
}
/**
* prepareInsertSQL
*/
private int prepareInsertSQL(RowData rd, Map<String, Object> outs,
StringBuilder strBuffer, boolean autoPopulatePrimaryKey,
boolean changedOnly) {
RowInfo ri = rd.getRowInfo();
if (ri == null)
throw new IllegalArgumentException("Error in prepareInsertSQL: no RowInfo.");
StringBuilder names = new StringBuilder();
StringBuilder values = new StringBuilder();
int maxSize = rd.getSize();
int positionIndex = 0;
ColumnInfo ci = null;
int i = 0;
for (i = 0; i < maxSize - 1; i++) {
ci = ri.getColumnInfo(i);
if (ci.isPrimaryKey() && autoPopulatePrimaryKey) continue;
if (ci.isReadOnly() || !ci.isWritable()) continue;
if (changedOnly && !modifiedColumns.contains(ci.getColumnName())) continue;
names.append(ci.getColumnName()).append(", ");
values.append("?, ");
positionIndex = positionIndex + 1;
outs.put(positionIndex+"", rd.getField(i));
}
//the last column: i=maxSize-1
ci = ri.getColumnInfo(i);
if (!ci.isReadOnly()
&& ci.isWritable()
&& (!changedOnly || changedOnly
&& modifiedColumns.contains(ci.getColumnName()))) {
names.append(ci.getColumnName()).append("");
values.append("?");
positionIndex = positionIndex + 1;
outs.put(positionIndex+"", rd.getField(i));
}
String namesList = names.toString();
if (namesList.endsWith(", ")) {
namesList = namesList.substring(0,namesList.lastIndexOf(','));
}
String valuesList = values.toString();
if (valuesList.endsWith(", ")) {
valuesList = valuesList.substring(0,valuesList.lastIndexOf(','));
}
strBuffer.append("(").append(namesList).append(") VALUES (").append(valuesList).append(")");
return positionIndex;
}
/**
* prepareWhereClause
*/
private int prepareWhereClause(int startPosition, Map<String, Object> ins, Map<String, Object> outs, StringBuilder strBuffer) {
int maxSize = ins.size();
int count = 0;
for (Map.Entry<String, Object> entry : ins.entrySet()) {
String keyName = entry.getKey();
Object valueData = entry.getValue();
count = count + 1;
if (maxSize != count)
strBuffer.append(keyName).append(" = ? AND ");
else
strBuffer.append(keyName).append(" = ? ");
outs.put(startPosition+"", valueData);
startPosition = startPosition + 1;
}
return startPosition;
}
/**
* prepareSetSQL
*/
private int prepareSetSQL(int startPosition, RowData rd,
Map<String, Object> outs, StringBuilder strBuffer,
boolean changedOnly) {
RowInfo ri = rd.getRowInfo();
if (ri == null)
throw new IllegalArgumentException("Error in prepareSetSQL: no RowInfo.");
int maxSize = rd.getSize();
int i = 0;
ColumnInfo ci = null;
for (i = 0; i < maxSize; i++) {
ci = ri.getColumnInfo(i);
if (ci.isReadOnly() || !ci.isWritable() || ci.isPrimaryKey()) continue;
if (changedOnly && !modifiedColumns.contains(ci.getColumnName())) continue;
strBuffer.append(ci.getColumnName()).append(" = ?, ");
outs.put(startPosition + "", rd.getField(i));
startPosition = startPosition + 1;
}
return startPosition;
}
/**
* <p>Populates primary key values based on the primary key rules.</p>
*
* <p>Override getPrimaryKeyRule() to override default rule.</p>
*
* @return Map representing name and value pairs of primary keys
*/
private Map<String, Object> populatePrimaryKeyValuesBeforeInsert() {
Map<String, Object> primaryKeyRules = getPrimaryKeyRules();
if (primaryKeyRules == null || primaryKeyRules.size() == 0) {
return null;//choose to let database auto generate pk
}
Map<String, Object> pkValues = new HashMap<String, Object>();
for (Map.Entry<String, Object> entry : primaryKeyRules.entrySet()) {
String pkName = entry.getKey();
Object pkValue = entry.getValue();
if (pkValue instanceof String) {
String pkRule = (String)pkValue;
if (pkRule.toUpperCase().startsWith("SQLKEY")) {
String sqlkey = pkRule.substring(pkRule.indexOf("=")+1);
pkValues.put(pkName, SqlServiceClient.retrieveObjectBySQLKey(sqlkey));
}
else
if (pkRule.toUpperCase().startsWith("SQL")) {
String sql = pkRule.substring(pkRule.indexOf("=")+1);
pkValues.put(pkName, SqlServiceClient.retrieveObjectBySQL(sql));
}
else
if (pkRule.toUpperCase().startsWith("SELECT")) {
pkValues.put(pkName, SqlServiceClient.retrieveObjectBySQL(pkRule));
}
else {
pkValues.put(pkName, pkValue);
}
}
else {
pkValues.put(pkName, pkValue);
}
}
return pkValues;
}
private boolean isPrimaryKeyDataEmpty() {
Map<String, Object> pkMap = getPrimaryKeyDataMap();
if (pkMap == null || pkMap.size() == 0) return true;
//make sure every pk column has data
boolean check = false;
for (Map.Entry<String, Object> entry : pkMap.entrySet()) {
Object keyValue = entry.getValue();
if (keyValue == null) {
check = true;
break;
}
}
return check;
}
/**
* Creates a backup copy of the current record data
*/
protected void beforeSetData() {
if (hasCopied) return;
if (latestDbRowData == null) {
latestDbRowData = new RowData(rowInfo, copyArray(rowData.getFields()));
}
else {
latestDbRowData.setFields(copyArray(rowData.getFields()));
}
hasCopied = true;//only copy data when the first setData is called.
}
private Object[] copyArray(Object[] data) {
if (data == null) return null;
Object[] dest = new Object[data.length];
System.arraycopy(data, 0, dest, 0, data.length);
return dest;
}
/**
* Records those columns that are modified and sets dirty flag.
*
* @param modifiedColumnNames
*/
protected void afterSetData(List<String> modifiedColumnNames) {
if (modifiedColumnNames == null || modifiedColumnNames.size() == 0) return;
addToModifiedColumnNames(modifiedColumnNames);
dirty = true;
}
/**
* Records the column index that is modified and sets dirty flag
*
* @param columnIndex
*/
protected void afterSetData(int columnIndex) {
addToModifiedColumnNames(rowInfo.getColumnName(columnIndex));
dirty = true;
}
/**
* Records the column that is modified and sets dirty flag
*
* @param columnName
*/
protected void afterSetData(String columnName) {
if (columnName == null) return;
addToModifiedColumnNames(columnName.toUpperCase());
dirty = true;
}
/**
* Do something before the record is found in database.
*/
protected void beforeFind() {
;
}
/**
* Do something after the record is found in database.
*/
protected void afterFind() {
;
}
/**
* Do something before the record is created in database.
*/
protected void beforeCreate() {
performValidationBeforeCreate();
}
/**
* Do something after the record is created in database.
*/
protected void afterCreate() {
}
/**
* Creates the record in database and returns it.
*/
protected ActiveRecord internal_create(boolean changedOnly) {
if (changedOnly && (modifiedColumns == null || modifiedColumns.size() == 0))
changedOnly = false;
String createSQL = "INSERT INTO " + getTableName();
Map<String, Object> pkValues = null;
try {
before_internal_create();
boolean autoPopulatePrimaryKey = false;
//prepare primary key value
if (isPrimaryKeyDataEmpty()) {
pkValues = populatePrimaryKeyValuesBeforeInsert();
if (pkValues == null || pkValues.size() == 0) {
autoPopulatePrimaryKey = true;
}
else {
setData(pkValues);
}
}
StringBuilder strBuffer = new StringBuilder();
Map<String, Object> inputs = new HashMap<String, Object>();
prepareInsertSQL(rowData, inputs, strBuffer, autoPopulatePrimaryKey, changedOnly);
createSQL += " " + strBuffer.toString();
log.debug("create sql = " + createSQL);
inputs = addMoreProperties(inputs, null);
OmniDTO returnTO =
getSqlService().execute(inputs, DataProcessorTypes.DIRECT_SQL_STATEMENT_PROCESSOR, createSQL);
int count = returnTO.getUpdatedRowCount();
if (count != 1) {
log.error("Only one record should be created, but " + count +
" objects were created instead.");
}
//populate auto-generated primary keys
if (autoPopulatePrimaryKey) {
long gpk = returnTO.getGeneratedKey();
if (gpk != -1) {
Map<String, Object> pkMap = getPrimaryKeyDataMap();
Iterator<String> it = pkMap.keySet().iterator();
if(it.hasNext()) { //only one column is allowed to be auto-generated primary key
setData((String)it.next(), Long.valueOf(gpk));
}
}
}
createClean();
after_internal_create();
}
catch (Exception ex) {
throw new BaseSQLException(ex);
}
return this;
}
/**
* <p>Cleans up something before finishing create.</p>
*
* <p>This is a fresh record now.</p>
*/
private void createClean() {
//remove backup copy
hasCopied = false;
latestDbRowData = null;
modifiedColumns.clear();
dirty = false;
existInDatabase = true;
}
/**
* Do something before the record is deleted in database.
*/
protected void beforeDelete() {
performValidationBeforeDelete();
}
/**
* Do something after the record is deleted in database.
*/
protected void afterDelete() {
;
}
/**
* <p>Do something before the internal_delete().</p>
*
* <p>For a has-one or has-many relation, either the foreign key in its
* associated records should be set to null, or the associated records
* should be removed if it is a dependent relation.</p>
*
*/
private void before_internal_delete() {
List<Relation> relations = RelationManager.getInstance().getOwnedRelations(getClass());
if (relations == null) return;
for (Relation rel : relations) {
if (rel == null) continue;
String type = rel.getRelationType();
if (Relation.HAS_MANY_TYPE.equals(type)) {
if (rel.allowCascadeNullify()) {
unhookHasMany(rel);
}
else if (rel.allowCascadeDelete()) {
deleteHasMany(rel);
}
else if (rel.allowCascadeSimplyDelete()) {
deleteHasManySimply(rel);
}
//else no cascade effect
}
else if (Relation.HAS_ONE_TYPE.equals(type)) {
if (rel.allowCascadeNullify()) {
unhookHasOne(rel);
}
else if (rel.allowCascadeDelete()) {
deleteHasOne(rel);
}
else if (rel.allowCascadeSimplyDelete()) {
deleteHasOneSimply(rel);
}
//else no cascade effect
}
}
}
/**
* <p>Do something after the internal_delete().</p>
*
* <p>For a belongs-to relation, decrement counter field in its parent object
* if there is a counter field.</p>
*/
private void after_internal_delete() {
List<Relation> relations = RelationManager.getInstance().getOwnedRelations(getClass());
if (relations == null) return;
for (Relation rel : relations) {
String type = rel.getRelationType();
if (Relation.BELONGS_TO_TYPE.equals(type)) {
decrementCounterInParent((BelongsToRelation)rel);
}
}
}
/**
* Do something before the record is saved in database.
*/
protected void beforeSave() {
performValidationBeforeSave();
}
/**
* Do something after the record is saved in database.
*/
protected void afterSave() {
;
}
/**
* Do something before the record is updated in database.
*/
protected void beforeUpdate() {
performValidationBeforeUpdate();
}
/**
* Do something after the record is updated in database.
*/
protected void afterUpdate() {
;
}
/**
* Updates the record.
*
* @param changedOnly true if only changed fields are included in SQL query
* @return count of records updated.
*/
private int internal_update(boolean changedOnly) {
if (changedOnly
&& (modifiedColumns == null || modifiedColumns.size() == 0))
return 0;
before_internal_update();
int count = 0;
String updateSQL = "UPDATE " + getTableName();
try {
Map<String, Object> inputs = new HashMap<String, Object>();
int position = 1;
//construct sets
StringBuilder sets = new StringBuilder();
position = prepareSetSQL(position, rowData, inputs, sets, changedOnly);
sets = StringUtil.removeLastToken(sets, ", ");
updateSQL += " SET " + sets.toString();
//construct where clause
Map<String, Object> conditions = null;
String[] pkNames = rowInfo.getPrimaryKeyColumnNames();
if (pkNames == null || pkNames.length == 0) {
conditions = (latestDbRowData != null)?latestDbRowData.getDataMap():null;
}
else {
conditions = rowData.getPrimaryKeyDataMap();
}
if (conditions != null && conditions.size() > 0) {
StringBuilder wheres = new StringBuilder();
position = prepareWhereClause(position, conditions, inputs, wheres);
updateSQL += " WHERE " + wheres.toString();
}
log.debug("update sql = " + updateSQL);
inputs = addMoreProperties(inputs, null);
OmniDTO returnTO =
getSqlService().execute(inputs, DataProcessorTypes.DIRECT_SQL_STATEMENT_PROCESSOR, updateSQL);
count = returnTO.getUpdatedRowCount();
after_internal_update();
}
catch (Exception ex) {
throw new BaseSQLException(ex);
}
return count;
}
/**
* <p>Cleans up something before finishing update.</p>
*
* <p>This is a fresh record now.</p>
*/
private void updateClean() {
//remove backup copy
hasCopied = false;
latestDbRowData = null;
modifiedColumns.clear();
dirty = false;
}
/**
* <p>Do something before the internal_create().</p>
*
* <p>For a belongs-to relation, if the parent is new or dirty, the parent
* must be saved first. A dirty parent may have an updated foreign-key
* value which needs to be updated in child record.</p>
*/
private void before_internal_create() {
processAutoAuditCreate();
for (Map.Entry<String, RecordRelation> entry : recordRelations.entrySet()) {
RecordRelation rr = recordRelations.get(entry.getKey());
if (rr == null) continue;
String type = rr.getRelation().getRelationType();
if (Relation.BELONGS_TO_TYPE.equals(type)) {
AssociatedRecord assR = (AssociatedRecord)rr.getAssociatedData();
if (assR != null) {
ActiveRecord parent = assR.getRecord();
if (parent != null) {
if (parent.isNewRecord() || parent.isDirty()) {
parent.save();
}
AssociationHelper.populateFKInBelongsTo(this, rr.getRelation().getMappingMap(), parent);
}
}
}
}
}
/**
* <p>Do something for the auto-audit update fields.</p>
*
* <p>Subclass can override this method if needed.</p>
*/
protected void processAutoAuditCreate() {
if (DatabaseConfig.getInstance().allowAutoAuditCreate()) {
if (rowInfo != null) {
String[] colNames = rowInfo.getColumnNames();
if (colNames != null) {
int length = colNames.length;
for (int i=0; i<length; i++) {
String colName = colNames[i];
if (DatabaseConfig.getInstance().isAutoAuditCreate(colName) ||
DatabaseConfig.getInstance().isAutoAuditUpdate(colName)) {
setData(colName, getCurrentTimestamp());
}
}
}
}
}
}
/**
* <p>Do something for the auto-audit update fields.</p>
*
* <p>Subclass can override this method if needed.</p>
*/
protected void processAutoAuditUpdate() {
if (DatabaseConfig.getInstance().allowAutoAuditUpdate()) {
if (rowInfo != null) {
String[] colNames = rowInfo.getColumnNames();
if (colNames != null) {
int length = colNames.length;
for (int i=0; i<length; i++) {
String colName = colNames[i];
if (DatabaseConfig.getInstance().isAutoAuditUpdate(colName)) {
setData(colName, getCurrentTimestamp());
}
}
}
}
}
}
private Timestamp getCurrentTimestamp() {
return new Timestamp(System.currentTimeMillis());
}
/**
* <p>Do something after the internal_create().</p>
*
* <p>For a has-one and has-many relation, sets foreign-key of associated
* objects. This is because when the owner object is newly created,
* the associated objects may not have the foreign-key column filled.</p>
*
* <p>For a belongs-to relation, increment counter field in its parent object
* if there is a counter field.</p>
*/
private void after_internal_create() {
List<Relation> relations = RelationManager.getInstance().getOwnedRelations(getClass());
if (relations == null) return;
for (Relation rel : relations) {
if (rel == null) continue;
String type = rel.getRelationType();
if (Relation.HAS_MANY_TYPE.equals(type)) {
hookupHasMany(rel);
}
else if (Relation.HAS_ONE_TYPE.equals(type)) {
hookupHasOne(rel);
}
else if (Relation.BELONGS_TO_TYPE.equals(type)) {
incrementCounterInParent((BelongsToRelation)rel);
}
}
}
/**
* <p>Sets up foreign-key link in child record. This is equivalent to
* execute this SQL statement:
*
* <blockquote>update items set order_id = 1 where id = 10</blockquote>
* </p>
*
* @param rel relation
*/
private void hookupHasOne(Relation rel) {
//set FK in otherRecord--the associated object, based on owner's PK data
if (rel == null) return;
RecordRelation rr = getRecordRelation(rel.getAssociation());
AssociatedRecord assR = (AssociatedRecord)rr.getAssociatedData();
if (assR != null) {
ActiveRecord target = assR.getRecord();
if (target != null) {
Map<String, Object> fkData = rr.getFKDataMapForOther();
if (fkData == null) return;
if (target.isNewRecord() || fkChangable(target, fkData)) {
target.setData(fkData);
}
if (target.isDirty()) {
target.save();
}
}
}
}
/**
* <p>Sets up foreign-key link in child records. This is equivalent to
* execute this SQL statement for each child record:
*
* <blockquote>update items set order_id = 1 where id = 10</blockquote>
* </p>
*
* @param rel relation
*/
private void hookupHasMany(Relation rel) {
if (rel == null) return;
RecordRelation rr = getRecordRelation(rel.getAssociation());
AssociatedRecords assRs = (AssociatedRecords)rr.getAssociatedData();
if (assRs != null) {
List<ActiveRecord> list = assRs.getRecords();
if (list != null) {
Map<String, Object> fkData = rr.getFKDataMapForOther();
if (fkData == null) return;
for (int i=0; i<list.size(); i++) {
ActiveRecord target = list.get(i);
if (target != null) {
if (target.isNewRecord() || fkChangable(target, fkData)) {
target.setData(fkData);
}
if (target.isDirty()) {
target.save();
}
}
}
}
}
}
private void deleteHasOne(Relation rel) {
//set FK in otherRecord--the associated object, based on owner's PK data
if (rel == null) return;
AssociatedRecord ar = associated(rel.getAssociation(), true);
if (ar != null) {
ActiveRecord target = ar.getRecord();
if (target != null) {
target.delete();
}
}
}
//It is important to delete each child individually, because each may hook
//with other records.
//For example:
//delete from items where order_id = 1
private void deleteHasMany(Relation rel) {
if (rel == null) return;
AssociatedRecords ars = allAssociated(rel.getAssociation(), true);
//forced refresh is required otherwise it returns ars.size = 0;
if (ars != null) {
List<ActiveRecord> list = ars.getRecords();
if (list != null) {
for (int i=0; i<list.size(); i++) {
ActiveRecord target = list.get(i);
if (target != null) {
target.delete();
}
}
}
}
}
/**
* <p>Deletes child record without triggering callbacks. The example SQL
* statement:
*
* <blockquote>delete from items where order_id = 1</blockquote>
* </p>
*
* @param rel relation
*/
private void deleteHasOneSimply(Relation rel) {
deleteHasManySimply(rel);
}
/**
* <p>Deletes child records without triggering callbacks. The example SQL
* statement:
*
* <blockquote>delete from items where order_id = 1</blockquote>
* </p>
*
* @param rel relation
*/
private void deleteHasManySimply(Relation rel) {
if (rel == null) return;
ActiveRecord childHome = ActiveRecordUtil.getHomeInstance(rel.getTargetClass());
String childTable = childHome.getTableName();
String[] lhsFlds = rel.getLeftSideMappingItems();
String[] rhsFlds = rel.getRightSideMappingItems();
StringBuilder sb = new StringBuilder();
sb.append("DELETE FROM ").append(childTable);
StringBuilder whereSB = new StringBuilder();
Map<String, Object> inputs = new HashMap<String, Object>();
int size = lhsFlds.length;
for (int i=0; i<size; i++) {
String fkFld = rhsFlds[i];
String pkFld = lhsFlds[i];
Object fkData = getField(pkFld);
whereSB.append(fkFld).append(" = ").append("? AND ");
inputs.put((i+1)+"", fkData);
}
String whereStr = StringUtil.removeLastToken(whereSB.toString(), "AND ");
String deleteSQL = sb.append(" WHERE ").append(whereStr).toString();
log.debug("deleteHasManySimply deleteSQL: " + deleteSQL);
inputs = addMoreProperties(inputs, null);
ActiveRecordUtil.getGateway(getClass()).deleteBySQL(deleteSQL, inputs);
}
/**
* <p>Removes foreign-key link in child records. The example sql
* statement:
*
* <blockquote>update items set order_id = null where order_id = 1</blockquote>
* </p>
*
* <p>If foreign-key column is not nullable, an exception will be thrown.</p>
*
* @param rel relation
*/
private void unhookHasOne(Relation rel) {
unhookHasMany(rel);
}
/**
* <p>Removes foreign-key link in child records. The example sql
* statement:
*
* <blockquote>update items set order_id = null where order_id = 1</blockquote>
* </p>
*
* <p>If foreign-key column is not nullable, an exception will be thrown.</p>
*
* @param rel relation
*/
private void unhookHasMany(Relation rel) {
//set FK in children, based on owner's PK data
if (rel == null) return;
//need to verify the foreign key field is nullable.
ActiveRecord childHome = ActiveRecordUtil.getHomeInstance(rel.getTargetClass());
String childTable = childHome.getTableName();
String[] lhsFlds = rel.getLeftSideMappingItems();
String[] rhsFlds = rel.getRightSideMappingItems();
StringBuilder sb = new StringBuilder();
sb.append("UPDATE ").append(childTable).append(" SET ");
StringBuilder setSB = new StringBuilder();
StringBuilder whereSB = new StringBuilder();
Map<String, Object> inputs = new HashMap<String, Object>();
int size = lhsFlds.length;
for (int i = 0; i < size; i++) {
String fkFld = rhsFlds[i];
if (childHome.isRequiredColumn(fkFld)) {
throw new GenericException("Column " + fkFld + " in table " + childTable + " cannot be nullified.");
}
String pkFld = lhsFlds[i];
Object fkData = getField(pkFld);
setSB.append(fkFld).append(" = NULL, ");
whereSB.append(fkFld).append(" = ").append("? AND ");
inputs.put((i+1)+"", fkData);
}
String setStr = StringUtil.removeLastToken(setSB.toString(), ", ");
String whereStr = StringUtil.removeLastToken(whereSB.toString(), "AND ");
String updateSQL = sb.append(setStr).append(" WHERE ").append(whereStr).toString();
inputs = addMoreProperties(inputs, null);
ActiveRecordUtil.getGateway(getClass()).updateBySQL(updateSQL, inputs);
}
void incrementCounterInParent(BelongsToRelation btr) {
if (btr == null || isNewRecord()) return;
if (btr.hasCounterCache()) {
ActiveRecord parentRecord = associated(btr.getAssociation()).getRecord();
if (parentRecord != null) {
parentRecord.incrementCounter(btr.getCounterCacheName());
}
}
}
void decrementCounterInParent(BelongsToRelation btr) {
if (btr == null || isNewRecord()) return;
if (btr.hasCounterCache()) {
ActiveRecord parentRecord = associated(btr.getAssociation()).getRecord();
if (parentRecord != null) {
parentRecord.decrementCounter(btr.getCounterCacheName());
}
}
}
/**
* <p>Do something before the internal_update().</p>
*
* <p>For a belongs-to relation, if the parent is new or dirty, the parent
* must be saved first. A dirty parent may have an updated foreign-key
* value which needs to be updated in child record.</p>
*/
private void before_internal_update() {
processAutoAuditUpdate();
for (Map.Entry<String, RecordRelation> entry : recordRelations.entrySet()) {
RecordRelation rr = (RecordRelation)recordRelations.get(entry.getKey());
if (rr == null) continue;
String type = rr.getRelation().getRelationType();
if (Relation.BELONGS_TO_TYPE.equals(type)) {
AssociatedRecord assR = (AssociatedRecord)rr.getAssociatedData();
if (assR != null) {
ActiveRecord parent = assR.getRecord();
if (parent != null) {
if (parent.isNewRecord() || parent.isDirty()) {
parent.save();
}
AssociationHelper.populateFKInBelongsTo(this, rr.getRelation().getMappingMap(), parent);
}
}
}
}
}
/**
* <p>Do something after the internal_update().</p>
*
* <p>For a has-one and has-many relation, sets foreign-key of associated
* objects. This is because the associated objects may not have the
* foreign-key column filled or the associated objects may be dirty.</p>
*/
private void after_internal_update() {
//do some maintenance works
updateClean();
for (Map.Entry<String, RecordRelation> entry : recordRelations.entrySet()) {
RecordRelation rr = (RecordRelation)recordRelations.get(entry.getKey());
if (rr == null) continue;
String type = rr.getRelation().getRelationType();
if (Relation.HAS_MANY_TYPE.equals(type)) {
updateAssociatedHasMany(rr);
}
else if (Relation.HAS_ONE_TYPE.equals(type)) {
updateAssociatedHasOne(rr);
}
}
}
/**
* Updates associated record if it has been modified.
*
* @param rr RecordRelation
*/
private void updateAssociatedHasOne(RecordRelation rr) {
if (rr == null) return;
AssociatedRecord assR = (AssociatedRecord)rr.getAssociatedData();
if (assR != null) {
ActiveRecord target = assR.getRecord();
if (target != null) {
Map<String, Object> fkData = rr.getFKDataMapForOther();
if (target.isNewRecord() || fkChangable(target, fkData)) {
target.setData(fkData);
}
if (target.isDirty()) {
target.save();
}
}
}
}
/**
* Updates associated records if they have been modified.
*
* @param rr RecordRelation
*/
private void updateAssociatedHasMany(RecordRelation rr) {
//set FK in children, based on owner's PK data
if (rr == null) return;
AssociatedRecords assRs = (AssociatedRecords)rr.getAssociatedData();
if (assRs != null) {
List<ActiveRecord> list = assRs.getRecords();
if (list != null) {
Map<String, Object> fkData = rr.getFKDataMapForOther();
for (int i=0; i<list.size(); i++) {
ActiveRecord target = (ActiveRecord)list.get(i);
if (target != null) {
if (target.isNewRecord() || fkChangable(target, fkData)) {
target.setData(fkData);
}
if (target.isDirty()) {
target.save();
}
}
}
}
}
}
private boolean fkChangable(ActiveRecord target, Map<String, Object> fkData) {
boolean status = false;
for (Map.Entry<String, Object> entry : fkData.entrySet()) {
String keyColumn = entry.getKey();
Object value = entry.getValue();
Object targetValue = target.getField(keyColumn);
if (value != null) {
if (!value.toString().equals(targetValue.toString())) {
status = true;
break;
}
}
else {
if (targetValue != null) {
status = true;
break;
}
}
}
return status;
}
private void addToModifiedColumnNames(String name) {
if (name == null) return;
name = name.toUpperCase();
if (!modifiedColumns.contains(name)) modifiedColumns.add(name);
}
private void addToModifiedColumnNames(List<String> names) {
if (names == null) return;
for (String name : names) {
addToModifiedColumnNames(name);
}
}
/**
* <p>Acts as a certain type in a category. </p>
*
* <p>This method adds a hasMany association with the target in the owner
* class, and a belongsTo association in the target class. </p>
*
* <p>The <tt>target</tt> class is the center class of the category.</p>
*
* <pre>
* Examples:
* owner class => Image, File, Post
* category => taggable
* target class => Tagging
* </pre>
*
* @param category the category this model performs
* @param target the associated class
*/
public void actAsInCategory(String category, Class<? extends ActiveRecord> target) {
String type = ActiveRecordUtil.getModelName(getClass());
actAsInCategory(type, category, target);
}
/**
* <p>Acts as a certain type in a category. </p>
*
* <p>This method adds a hasMany association with the target in the owner
* class, and a belongsTo association in the target class. </p>
*
* <p>This method assumes that you use "id" as primary key in the owner entity.</p>
*
* <pre>
* Examples:
* owner class => Image, File, Post
* type => image, file, post
* category => taggable
* target class => Tagging
* </pre>
*
* @param type the specific type this entity represents
* @param category the category this entity performs
* @param target the associated class
*/
public void actAsInCategory(String type, String category, Class<? extends ActiveRecord> target) {
//make sure category center is loaded first
RelationManager.getInstance().registerRelations(target);
Category categoryInstance = RelationManager.getInstance().getCategory(category);
if (categoryInstance == null) {
throw new UnregisteredCategoryException(category);
}
String idField = categoryInstance.getIdField();
String typeField = categoryInstance.getTypeField();
String cTableName = ActiveRecordUtil.getTableName(target);
String associationType = Relation.HAS_MANY_TYPE;
String bcProperties = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " +
ActiveRecordConstants.key_conditions_sql + ": " + cTableName + "." + typeField + "='" + type + "'; cascade: delete";
String cbProperties = ActiveRecordConstants.key_mapping + ": " + idField + "=id; ";
actAsInCategory(type, category, associationType, target, bcProperties, cbProperties);
}
/**
* <p>Acts as a certain type in a category. </p>
*
* <p>This method adds a hasMany (or hasOne) association with the target
* in the owner class, and a belongsTo association in the target class. The
* target class is the center of the category.</p>
*
* <p>Assuming owner class is B, target class is C,
* bcProperties is join properties from B to C,
* cbProperties is join properties from C to B.</p>
*
* <pre>
* Examples:
* owner class => Image, File, Post
* type => image, file, post
* category => taggable
* target class => Tagging
* </pre>
*
* @param type the specific type this entity represents in the category
* @param category the category this entity performs
* @param relationType either has-many or has-one
* @param target the associated class
* @param bcProperties properties of the has-many or has-one association from owner to target
* @param cbProperties properties of the belongs-to association from target to owner
*/
public void actAsInCategory(String type, String category, String relationType,
Class<? extends ActiveRecord> target, String bcProperties, String cbProperties) {
//make sure category center is loaded first
RelationManager.getInstance().registerRelations(target);
//register the entity in category:
Category categoryInstance = RelationManager.getInstance().getCategory(category);
if (categoryInstance == null) {
throw new UnregisteredCategoryException(category);
}
String idField = categoryInstance.getIdField();
String typeField = categoryInstance.getTypeField();
String cTableName = ActiveRecordUtil.getTableName(target);
if (type == null) type = ActiveRecordUtil.getModelName(getClass());
categoryInstance.addEntity(type, ActiveRecordUtil.getModelName(getClass()));
String throughTypeCondition = ActiveRecordConstants.key_conditions_sql + ": " + cTableName + "." + typeField + "='" + type + "'";
if (bcProperties == null) {
bcProperties = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " +
throughTypeCondition + "; cascade: delete";
}
else {
if (bcProperties.indexOf(ActiveRecordConstants.key_conditions_sql) == -1) {
bcProperties = throughTypeCondition + "; " + bcProperties;
}
if (bcProperties.indexOf(ActiveRecordConstants.key_mapping) == -1) {
bcProperties = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " + bcProperties;
}
if (bcProperties.indexOf(ActiveRecordConstants.key_cascade) == -1) {
bcProperties = ActiveRecordConstants.key_cascade + ": delete" + "; " + bcProperties;
}
}
if (Relation.HAS_ONE_TYPE.equals(relationType)) {
hasOne(target, bcProperties);
}
else
if (Relation.HAS_MANY_TYPE.equals(relationType)) {
hasMany(target, bcProperties);
}
else {
throw new UndefinedRelationException("Relation type " + relationType + " is not allowed in actAsInCategory().");
}
//add the following associations in target:
String cbMappingProperty = ActiveRecordConstants.key_mapping + ": " + idField + "=id; ";
if (cbProperties == null) {
cbProperties = cbMappingProperty;
}
else {
if (cbProperties.indexOf(ActiveRecordConstants.key_mapping) == -1) {
cbProperties = cbMappingProperty + "; " + cbProperties;
}
}
ActiveRecord targetHome = ActiveRecordUtil.getHomeInstance(target);
targetHome.belongsTo(getClass(), cbProperties);
}
/**
* <p>Returns a RecordRelation related to the target model.</p>
*
* <p>The <tt>associationId</tt> is the name of the relation defined in the class.</p>
*
* @param associationId association id
* @return a RecordRelation related to the target model
*/
public RecordRelation getRecordRelation(String associationId) {
if (associationId == null) throw new IllegalArgumentException("association name is empty.");
//find the relationship type between this and model
associationId = associationId.toLowerCase();
RecordRelation rr = (RecordRelation)recordRelations.get(associationId);
if (rr == null) {
rr = RelationManager.getInstance().createRecordRelation(this, associationId);
recordRelations.put(associationId, rr);
}
return rr;
}
/**
* Sets a RecordRelation related to the target model.
*
* @param associationId association id
* @param rr RecordRelation related to the target model
*/
public void setRecordRelation(String associationId, RecordRelation rr) {
recordRelations.put(associationId, rr);
}
/**
* Sets fields to be protected.
*
* @param fields A string of field names separated by comma.
*/
protected void setProtectedFields(String fields) {
if (fields != null) {
protectedColumns.addAll(Converters.convertStringToList(fields.toUpperCase()));
}
}
/**
* Returns true if the field is a protected field.
*
* @param field
* @return boolean true if the field is a protected field.
*/
private boolean isProtectedField(String field) {
boolean pted = false;
if (field != null) {
pted = protectedColumns.contains(field.toUpperCase());
}
return pted;
}
private Map<String, ?> filterProtectedFields(Map<String, ?> dataMap) {
if (dataMap == null) return dataMap;
Map<String, Object> newDataMap = new HashMap<String, Object>();
for (Map.Entry<String, ?> entry : dataMap.entrySet()) {
String key = entry.getKey();
if (isProtectedField(key)) continue;
newDataMap.put(key, entry.getValue());
}
return newDataMap;
}
/**
* Sets fields to be extra.
*
* @param fields A string of field names separated by comma.
*/
protected void setExtraFields(String fields) {
if (fields != null) {
extraFields.addAll(Converters.convertStringToList(fields.toUpperCase()));
}
}
private Object getExtraFieldData(String fieldName) {
if (fieldName == null) return null;
return extraFieldsMap.get(fieldName.toUpperCase());
}
private void setExtraFieldData(String fieldName, Object data) {
if (fieldName == null) return;
extraFieldsMap.put(fieldName.toUpperCase(), data);
}
private Map<String, Object> getExtraFieldData(List<String> fieldNames) {
if (fieldNames == null) return null;
Map<String, Object> fieldData = new HashMap<String, Object>();
for (String field : fieldNames) {
if (isExtraField(field)) {
fieldData.put(field, getExtraFieldData(field));
}
}
return fieldData;
}
private List<String> setExtraFieldData(Map<String, Object> data) {
if (data == null) return null;
List<String> changedFields = new ArrayList<String>();
for (Map.Entry<String, Object> entry : data.entrySet()) {
String field = entry.getKey();
if (isExtraField(field)) {
setExtraFieldData(field, entry.getValue());
changedFields.add(field.toUpperCase());
}
}
return changedFields;
}
Map<String, Object> addMoreProperties(Map<String, Object> inputs, Map<String, String> options) {
if (inputs == null) inputs = new HashMap<String, Object>();
String connName = null;
if (options != null) {
connName = options.get(DataProcessor.input_key_database_connection_name);
}
if (connName != null) {
inputs.put(DataProcessor.input_key_database_connection_name, connName);
}
else {
inputs.put(DataProcessor.input_key_database_connection_name, getConnectionName());
}
return inputs;
}
/**
* Returns the <tt>ValidationResults</tt> instance of this record.
*
* @return ValidationResults
*/
public ValidationResults getValidationResults() {
return errors;
}
/**
* Returns <tt>true</tt> if the record has no error.
*/
public boolean isValid() {
return !errors.failed();
}
private void clearValidationResults() {
errors.clear();
}
private void performValidationBeforeCreate() {
clearValidationResults();
validatesRecordBeforeCreate();
if (errors.failed()) {
throw new RecordValidationException(errors);
}
}
private void performValidationBeforeUpdate() {
clearValidationResults();
validatesRecordBeforeUpdate();
if (errors.failed()) {
throw new RecordValidationException(errors);
}
}
private void performValidationBeforeSave() {
clearValidationResults();
validatesRecordBeforeSave();
if (errors.failed()) {
throw new RecordValidationException(errors);
}
}
private void performValidationBeforeDelete() {
clearValidationResults();
validatesRecordBeforeDelete();
if (errors.failed()) {
throw new RecordValidationException(errors);
}
}
/**
* <p>
* Subclass must override this method in order to provide a meaningful
* validation.
* </p>
*
* <p>
* The default implementation of this method is empty.
* </p>
*/
public void validatesRecord() {
;
}
/**
* This delegates to validatesRecord(). Subclass need to override this
* method in order to provide a meaningful validation.
*/
public void validatesRecordBeforeCreate() {
validatesRecord();
}
/**
* This delegates to validatesRecord(). Subclass need to override this
* method in order to provide a meaningful validation.
*/
public void validatesRecordBeforeUpdate() {
validatesRecord();
}
/**
* This delegates to validatesRecord(). Subclass need to override this
* method in order to provide a meaningful validation.
*/
public void validatesRecordBeforeSave() {
validatesRecord();
}
/**
* The default implementation actually does nothing. Subclass need to
* override this method in order to provide a meaningful validation.
*/
public void validatesRecordBeforeDelete() {
;
}
/**
* <p>Returns an instance of Calculations.</p>
*
* <p>Subclass must override this method if a different calculator is used.</p>
*
* @return Calculations object
*/
public Calculator getCalculator() {
return new Calculator(this);
}
/**
* <p>Returns an instance of Validators.</p>
*
* <p>Subclass must override this method if a different validator is used.</p>
*
* @return Validators object
*/
public ModelValidators validators() {
return (validators != null)?validators:(new ModelValidators(this));
}
/**
* Shows details of the record. This method returns much more information
* than the <tt>toString()</tt> method, such as table name, dirty, existed
* in database, etc.
*
* @return String
*/
public String details() {
StringBuilder returnString = new StringBuilder();
String separator = "\r\n";
returnString.append("tableName = " + tableName).append(separator);
returnString.append("existInDatabase = " + existInDatabase).append(separator);
returnString.append("freezed = " + freezed).append(separator);
returnString.append("dirty = " + dirty).append(separator);
returnString.append("hasCopied = " + hasCopied).append(separator);
returnString.append("modifiedColumns = " + modifiedColumns).append(separator);
returnString.append("errors = " + errors).append(separator);
returnString.append("protectedColumns = " + protectedColumns).append(separator);
returnString.append("extraFields = " + extraFields).append(separator);
returnString.append("extraFieldsMap = " + extraFieldsMap).append(separator);
returnString.append("recordRelations = " + recordRelations).append(separator);
returnString.append("rowInfo = (" + rowInfo).append(")").append(separator);
returnString.append("rowData = (" + rowData).append(")").append(separator);
return returnString.toString();
}
/**
* Returns a string representation of the record.
* @return String
*/
public String toString() {
StringBuilder sb = new StringBuilder();
String separator = ", ";
String[] colNames = rowInfo.getColumnNames();
int colNamesLength = colNames.length;
for (int i = 0; i < colNamesLength; i++) {
String colName = colNames[i];
sb.append(colName.toLowerCase()).append("=");
sb.append(getField(colName)).append(separator);
}
if (extraFields.size() > 0) {
for (String colName : extraFields) {
sb.append(colName.toLowerCase()).append("=");
sb.append(getField(colName)).append(separator);
}
}
sb = StringUtil.removeLastToken(sb, ", ");
return sb.toString();
}
/**
* Returns a Map representation of the record. The keys in the map are
* column names in lowercase.
*
* @return Map
*/
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<String, Object>();
String[] colNames = rowInfo.getColumnNames();
int colNamesLength = colNames.length;
for (int i = 0; i < colNamesLength; i++) {
String colName = colNames[i];
map.put(colName.toLowerCase(), getField(colName));
}
for (String colName : extraFields) {
map.put(colName.toLowerCase(), getField(colName));
}
return map;
}
/**
* Returns an XML representation of the object.
*
* <pre>
* Example:
* <post>
* <id>1234</id>
* <title>Scooter Rocks</title>
* <body>We love to use Scooter.</body>
* </post>
* </pre>
* @return xml string
*/
public String toXML() {
StringBuilder xmlSB = new StringBuilder();
String classNameInLowerCase = Util.getShortClassName(this.getClass()).toLowerCase();
xmlSB.append("<").append(classNameInLowerCase).append(">");
String[] colNames = rowInfo.getColumnNames();
for (String colName : colNames) {
String colNameInLowerCase = colName.toLowerCase();
xmlSB.append("<").append(colNameInLowerCase).append(">");
xmlSB.append(getField(colName));
xmlSB.append("</").append(colNameInLowerCase).append(">");
}
for (String extraFldName : extraFields) {
String extraFldNameInLowerCase = extraFldName.toLowerCase();
xmlSB.append("<").append(extraFldNameInLowerCase).append(">");
xmlSB.append(getField(extraFldName));
xmlSB.append("</").append(extraFldNameInLowerCase).append(">");
}
xmlSB.append("</").append(classNameInLowerCase).append(">");
return xmlSB.toString();
}
/**
* Returns a JSON representation of the object.
*
* @return a json string
*/
public String toJSON() {
return (new JSONObject(toMap())).toString();
}
/**
* Generated serialVersionUID
*/
private static final long serialVersionUID = 5503274145344099012L;
//connection name
private String connectionName = null;
//table name
private String tableName = null;
//simple table name
private String simpleTableName = null;
//boolean to indicate whether the record is a new record
private boolean existInDatabase = false;
//boolean to indicate whether the record is a home record
private boolean isHomeInstance = false;
//boolean to indicate whether the record is freezed
private boolean freezed = false;
//boolean to indicate whether the record is modified and unsaved
private boolean dirty = false;
//boolean to indicate whether a copy has been created for the current record
private boolean hasCopied = false;
/**
* list to record which columns are modified.
* All names are in upper case.
*/
private List<String> modifiedColumns = new ArrayList<String>();
//current database record meta info
private RowInfo rowInfo;
//representing data in a row from database which user may modify
private RowData rowData;
//representing the latest data in a row from database
private RowData latestDbRowData;
private ValidationResults errors = new ValidationResults();
/**
* <p>list of protected column names</p>
*
* <p>Data fields defined in the protectedColumns list are protected from
* being set in massive assignments, such as setData(Map). Instead these
* fields have to be set directly by using setData(String) or
* setData(String, Object).</p>
*/
private List<String> protectedColumns = new ArrayList<String>();
/**
* <p>list of extra fields</p>
*
* <p>Extra fields are fields that are needed by the model during a transaction
* process but not recorded in the database table.</p>
*
* <p>For example, password_confirmation.</p>
*/
private List<String> extraFields = new ArrayList<String>();
/**
* <p>map to store values of extra fields.</p>
*
* <p>All keys are in upper case.</p>
*/
private Map<String, Object> extraFieldsMap = new HashMap<String, Object>();
/**
* <p>contains relation with target entities.</p>
*
* <p>Key is model name in lower case. Value is a specific RecordRelation object.</p>
*/
private Map<String, RecordRelation> recordRelations = new ConcurrentHashMap<String, RecordRelation>();
private transient ModelValidators validators = null;
private transient LogUtil log = LogUtil.getLogger(this.getClass().getName());
}