/*
* Copyright (C) Tony Green, Litepal Framework Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.litepal.crud;
import static org.litepal.util.BaseUtility.changeCase;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.litepal.LitePalBase;
import org.litepal.crud.model.AssociationsInfo;
import org.litepal.exceptions.DataSupportException;
import org.litepal.exceptions.DatabaseGenerateException;
import org.litepal.util.BaseUtility;
import org.litepal.util.Const;
import org.litepal.util.DBUtility;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
/**
* This is the base class for CRUD component. All the common actions which can
* be shared with each function in CURD component will be put here.
*
* @author Tony Green
* @since 1.1
*/
abstract class DataHandler extends LitePalBase {
public static final String TAG = "DataHandler";
/**
* Instance of SQLiteDatabase, use to do the CRUD job.
*/
SQLiteDatabase mDatabase;
/**
* Store empty model instance. In case to create each time when checking
* field is with default value or not.
*/
private DataSupport tempEmptyModel;
/**
* Holds the AssociationsInfo which foreign keys in the current model.
*/
private List<AssociationsInfo> fkInCurrentModel;
/**
* Holds the AssociationsInfo which foreign keys in other models.
*/
private List<AssociationsInfo> fkInOtherModel;
/**
* Query the table of the given model, returning a model list over the
* result set.
*
* @param modelClass
* The model to compile the query against.
* @param columns
* A list of which columns to return. Passing null will return
* all columns, which is discouraged to prevent reading data from
* storage that isn't going to be used.
* @param selection
* A filter declaring which rows to return, formatted as an SQL
* WHERE clause (excluding the WHERE itself). Passing null will
* return all rows for the given table.
* @param selectionArgs
* You may include ?s in selection, which will be replaced by the
* values from selectionArgs, in order that they appear in the
* selection. The values will be bound as Strings.
* @param groupBy
* A filter declaring how to group rows, formatted as an SQL
* GROUP BY clause (excluding the GROUP BY itself). Passing null
* will cause the rows to not be grouped.
* @param having
* A filter declare which row groups to include in the cursor, if
* row grouping is being used, formatted as an SQL HAVING clause
* (excluding the HAVING itself). Passing null will cause all row
* groups to be included, and is required when row grouping is
* not being used.
* @param orderBy
* How to order the rows, formatted as an SQL ORDER BY clause
* (excluding the ORDER BY itself). Passing null will use the
* default sort order, which may be unordered.
* @param limit
* Limits the number of rows returned by the query, formatted as
* LIMIT clause. Passing null denotes no LIMIT clause.
* @param foreignKeyAssociations
* Associated classes which have foreign keys in the current
* model's table.
* @return A model list. The list may be empty.
*/
@SuppressWarnings("unchecked")
protected <T> List<T> query(Class<T> modelClass, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having, String orderBy, String limit,
List<AssociationsInfo> foreignKeyAssociations) {
List<T> dataList = new ArrayList<T>();
Cursor cursor = null;
try {
List<Field> supportedFields = getSupportedFields(modelClass.getName());
String tableName = getTableName(modelClass);
String[] customizedColumns = getCustomizedColumns(columns, foreignKeyAssociations);
cursor = mDatabase.query(tableName, customizedColumns, selection, selectionArgs,
groupBy, having, orderBy, limit);
if (cursor.moveToFirst()) {
do {
Constructor<?> constructor = findBestSuitConstructor(modelClass);
T modelInstance = (T) constructor
.newInstance(getConstructorParams(constructor));
giveBaseObjIdValue((DataSupport) modelInstance,
cursor.getLong(cursor.getColumnIndexOrThrow("id")));
setValueToModel(modelInstance, supportedFields, foreignKeyAssociations, cursor);
if (foreignKeyAssociations != null) {
setAssociatedModel((DataSupport) modelInstance);
}
dataList.add(modelInstance);
} while (cursor.moveToNext());
}
return dataList;
} catch (Exception e) {
throw new DataSupportException(e.getMessage());
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Handles the math query of the given table.
*
* @param tableName
* Which table to query from.
* @param columns
* A list of which columns to return. Passing null will return
* all columns, which is discouraged to prevent reading data from
* storage that isn't going to be used.
* @param conditions
* A filter declaring which rows to return, formatted as an SQL
* WHERE clause. Passing null will return all rows.
* @param type
* The type of the based on column.
* @return The result calculating by SQL.
*/
@SuppressWarnings("unchecked")
protected <T> T mathQuery(String tableName, String[] columns, String[] conditions, Class<T> type) {
BaseUtility.checkConditionsCorrect(conditions);
Cursor cursor = null;
T result = null;
try {
cursor = mDatabase.query(tableName, columns, getWhereClause(conditions),
getWhereArgs(conditions), null, null, null);
if (cursor.moveToFirst()) {
Class<?> cursorClass = cursor.getClass();
Method method = cursorClass.getMethod(genGetColumnMethod(type), int.class);
result = (T) method.invoke(cursor, 0);
}
} catch (Exception e) {
throw new DataSupportException(e.getMessage());
} finally {
if (cursor != null) {
cursor.close();
}
}
return result;
}
/**
* Assign the generated id value to {@link DataSupport#baseObjId}. This
* value will be used as identify of this model for system use.
*
* @param baseObj
* The class of base object.
* @param id
* The value of id.
*/
protected void giveBaseObjIdValue(DataSupport baseObj, long id) throws SecurityException,
NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
if (id > 0) {
DynamicExecutor.setField(baseObj, "baseObjId", id, DataSupport.class);
}
}
/**
* Iterate all the fields passed in. Each field calls
* {@link #putFieldsValueDependsOnSaveOrUpdate(DataSupport, Field, ContentValues)}
* if it's not id field.
*
* @param baseObj
* Current model to persist or update.
* @param supportedFields
* List of all supported fields.
* @param values
* To store data of current model for persisting or updating.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws SecurityException
*/
protected void putFieldsValue(DataSupport baseObj, List<Field> supportedFields,
ContentValues values) throws SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
for (Field field : supportedFields) {
if (!isIdColumn(field.getName())) {
putFieldsValueDependsOnSaveOrUpdate(baseObj, field, values);
}
}
}
/**
* This method deals with the putting values job into ContentValues. The
* ContentValues has <b>put</b> method to set data. But we do not know we
* should use which <b>put</b> method cause the field type isn't clear. So
* the reflection API is necessary here to put values into ContentValues
* with dynamically getting field type to put value.
*
* @param baseObj
* The class of base object.
* @param field
* Field to put into ContentValues.
* @param values
* To store data of current model for persisting or updating.
* @throws SecurityException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
protected void putContentValues(DataSupport baseObj, Field field, ContentValues values)
throws SecurityException, IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
Object fieldValue = takeGetMethodValueByField(baseObj, field);
if ("java.util.Date".equals(field.getType().getName()) && fieldValue != null) {
Date date = (Date) fieldValue;
fieldValue = date.getTime();
}
Object[] parameters = new Object[] { changeCase(field.getName()), fieldValue };
Class<?>[] parameterTypes = getParameterTypes(field, fieldValue, parameters);
DynamicExecutor.send(values, "put", parameters, values.getClass(), parameterTypes);
}
/**
* It finds the getter method by the field. For example, field name is age,
* getter method name will be getAge. Then invoke the getter method and
* return the value.
*
* @param dataSupport
* The model to get method from.
* @param field
* Use to generate getter method name.
* @return The value returned by getter method.
* @throws SecurityException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
protected Object takeGetMethodValueByField(DataSupport dataSupport, Field field)
throws SecurityException, NoSuchMethodException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException {
if (shouldGetOrSet(dataSupport, field)) {
String getMethodName = makeGetterMethodName(field);
return DynamicExecutor.send(dataSupport, getMethodName, null, dataSupport.getClass(),
null);
}
return null;
}
/**
* It finds the setter method by the field. For example, field name is age,
* setter method name will be setAge. Then invoke the setter method with
* necessary parameter.
*
* @param dataSupport
* The model to set method to.
* @param field
* Use to generate setter method name.
* @param parameter
* The parameter to invoke setter method.
* @throws SecurityException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
protected void putSetMethodValueByField(DataSupport dataSupport, Field field, Object parameter)
throws SecurityException, NoSuchMethodException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException {
if (shouldGetOrSet(dataSupport, field)) {
String setMethodName = makeSetterMethodName(field);
DynamicExecutor.send(dataSupport, setMethodName, new Object[] { parameter },
dataSupport.getClass(), new Class[] { field.getType() });
}
}
/**
* Find all the associated models of currently model. Then add all the
* associated models into {@link DataSupport#associatedModels} of baseObj.
*
* @param baseObj
* The class of base object.
* @param foreignKeyId
* The id value of foreign key.
*/
protected void analyzeAssociatedModels(DataSupport baseObj,
Collection<AssociationsInfo> associationInfos) {
try {
for (AssociationsInfo associationInfo : associationInfos) {
if (associationInfo.getAssociationType() == Const.Model.MANY_TO_ONE) {
new Many2OneAnalyzer().analyze(baseObj, associationInfo);
} else if (associationInfo.getAssociationType() == Const.Model.ONE_TO_ONE) {
new One2OneAnalyzer().analyze(baseObj, associationInfo);
} else if (associationInfo.getAssociationType() == Const.Model.MANY_TO_MANY) {
new Many2ManyAnalyzer().analyze(baseObj, associationInfo);
}
}
} catch (Exception e) {
throw new DataSupportException(e.getMessage());
}
}
/**
* Create an empty instance of baseObj if it hasn't created one yet. If
* there's already an empty model existed in {@link #tempEmptyModel}, no
* need to create a new one.
*
* @param baseObj
* Current model to update.
* @return An empty instance of baseObj.
*/
protected DataSupport getEmptyModel(DataSupport baseObj) {
if (tempEmptyModel != null) {
return tempEmptyModel;
}
String className = null;
try {
className = baseObj.getClassName();
Class<?> modelClass = Class.forName(className);
tempEmptyModel = (DataSupport) modelClass.newInstance();
return tempEmptyModel;
} catch (ClassNotFoundException e) {
throw new DatabaseGenerateException(DatabaseGenerateException.CLASS_NOT_FOUND
+ className);
} catch (InstantiationException e) {
throw new DataSupportException(className + DataSupportException.INSTANTIATION_EXCEPTION);
} catch (Exception e) {
throw new DataSupportException(e.getMessage());
}
}
/**
* Get the WHERE clause to apply when updating or deleting multiple rows.
*
* @param conditions
* A string array representing the WHERE part of an SQL
* statement.
* @return The WHERE clause to apply when updating or deleting multiple
* rows.
*/
protected String getWhereClause(String... conditions) {
if (isAffectAllLines((Object) conditions)) {
return null;
}
if (conditions != null && conditions.length > 0) {
return conditions[0];
}
return null;
}
/**
* Get the WHERE arguments to fill into where clause when updating or
* deleting multiple rows.
*
* @param conditions
* A string array representing the WHERE part of an SQL
* statement.
* @return The WHERE arguments to fill into where clause when updating or
* deleting multiple rows.
*/
protected String[] getWhereArgs(String... conditions) {
if (isAffectAllLines((Object) conditions)) {
return null;
}
if (conditions != null && conditions.length > 1) {
String[] whereArgs = new String[conditions.length - 1];
System.arraycopy(conditions, 1, whereArgs, 0, conditions.length - 1);
return whereArgs;
}
return null;
}
/**
* Check the passing conditions represent to affect all lines or not. <br>
* Do not pass anything to the conditions parameter means affect all lines.
*
* @param conditions
* An array representing the WHERE part of an SQL statement.
* @return Affect all lines or not.
*/
protected boolean isAffectAllLines(Object... conditions) {
if (conditions != null && conditions.length == 0) {
return true;
}
return false;
}
/**
* Get the where clause by the passed in id collection to apply multiple
* rows.
*
* @param ids
* The id collection.
* @return The where clause to execute.
*/
protected String getWhereOfIdsWithOr(Collection<Long> ids) {
StringBuilder whereClause = new StringBuilder();
boolean needOr = false;
for (long id : ids) {
if (needOr) {
whereClause.append(" or ");
}
needOr = true;
whereClause.append("id = ");
whereClause.append(id);
}
return changeCase(whereClause.toString());
}
/**
* Get the where clause by the passed in id array to apply multiple rows.
*
* @param ids
* The id collection.
* @return The where clause to execute.
*/
protected String getWhereOfIdsWithOr(long... ids) {
StringBuilder whereClause = new StringBuilder();
boolean needOr = false;
for (long id : ids) {
if (needOr) {
whereClause.append(" or ");
}
needOr = true;
whereClause.append("id = ");
whereClause.append(id);
}
return changeCase(whereClause.toString());
}
/**
* Do not suggest use this method to find DataSupport class from hierarchy.
* Try to use DataSupport.class directly.
*
* Detect the baseObj is an instance of DataSupport or not. If true, return
* the class of DataSupport. Otherwise throw an exception of
* DataSupportException to tell user baseObj is not an instance of
* DataSupport.
*
* @param baseObj
* The base object model.
* @return The class of DataSupport or throw DataSupportException.
* @throws DataSupportException
*/
@Deprecated
protected Class<?> findDataSupportClass(DataSupport baseObj) {
Class<?> superClass = null;
while (true) {
superClass = baseObj.getClass().getSuperclass();
if (superClass == null || DataSupport.class == superClass) {
break;
}
}
if (superClass == null) {
throw new DataSupportException(baseObj.getClass().getName()
+ DataSupportException.MODEL_IS_NOT_AN_INSTANCE_OF_DATA_SUPPORT);
}
return superClass;
}
/**
* When executing {@link #takeGetMethodValueByField(DataSupport, Field)} or
* {@link #putSetMethodValueByField(DataSupport, Field, Object)}, the
* dataSupport and field passed in should be protected from null value.
*
* @param dataSupport
* The object to execute set or get method.
* @param field
* The field of generating set and get methods.
* @return True if dataSupport and field are not null, false otherwise.
*/
protected boolean shouldGetOrSet(DataSupport dataSupport, Field field) {
return dataSupport != null && field != null;
}
/**
* Get the name of intermediate join table.
*
* @param baseObj
* Current model.
* @param associatedTableName
* The name of associated table.
* @return The name of intermediate join table.
*/
protected String getIntermediateTableName(DataSupport baseObj, String associatedTableName) {
return changeCase(DBUtility.getIntermediateTableName(baseObj.getTableName(),
associatedTableName));
}
/**
* Get the simple name of modelClass. Then change the case by the setting
* rule in litepal.xml as table name.
*
* @param modelClass
* Class of model to get table name from.
* @return The table name of model.
*/
protected String getTableName(Class<?> modelClass) {
return BaseUtility.changeCase(modelClass.getSimpleName());
}
/**
* Finds the best suit constructor for creating an instance of a class. The
* principle is that constructor with least parameters will be the best suit
* one to create instance. So this method will find the constructor with
* least parameters of the class passed in.
*
* @param modelClass
* To get constructors from.
* @return The best suit constructor with least parameters.
*/
protected Constructor<?> findBestSuitConstructor(Class<?> modelClass) {
Constructor<?> finalConstructor = null;
Constructor<?>[] constructors = modelClass.getConstructors();
for (Constructor<?> constructor : constructors) {
if (finalConstructor == null) {
finalConstructor = constructor;
} else {
int finalParamLength = finalConstructor.getParameterTypes().length;
int newParamLength = constructor.getParameterTypes().length;
if (newParamLength < finalParamLength) {
finalConstructor = constructor;
}
}
}
finalConstructor.setAccessible(true);
return finalConstructor;
}
/**
* Depends on the passed in constructor, creating a parameters array with
* initialized values for the constructor.
*
* @param constructor
* The constructor to get parameters for it.
*
* @return A parameters array with initialized values.
*/
protected Object[] getConstructorParams(Constructor<?> constructor) {
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] params = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
params[i] = getInitParamValue(paramTypes[i]);
}
return params;
}
/**
* Get value from database by cursor, then set the value into modelInstance.
*
* @param modelInstance
* The model to set into.
* @param supportedFields
* Corresponding to each column in database.
* @param foreignKeyAssociations
* Associated classes which have foreign keys in the current
* model's table.
* @param cursor
* Use to get value from database.
* @throws SecurityException
* @throws IllegalArgumentException
* @throws NoSuchMethodException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
protected void setValueToModel(Object modelInstance, List<Field> supportedFields,
List<AssociationsInfo> foreignKeyAssociations, Cursor cursor) throws SecurityException,
IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
for (Field field : supportedFields) {
String getMethodName = genGetColumnMethod(field);
String columnName = isIdColumn(field.getName()) ? "id" : field.getName();
int columnIndex = cursor.getColumnIndex(BaseUtility.changeCase(columnName));
if (columnIndex != -1) {
Class<?> cursorClass = cursor.getClass();
Method method = cursorClass.getMethod(getMethodName, int.class);
Object value = method.invoke(cursor, columnIndex);
if (isIdColumn(field.getName())) {
DynamicExecutor.setField(modelInstance, field.getName(), value,
modelInstance.getClass());
} else {
if (field.getType() == boolean.class || field.getType() == Boolean.class) {
if ("0".equals(String.valueOf(value))) {
value = false;
} else if ("1".equals(String.valueOf(value))) {
value = true;
}
} else if (field.getType() == char.class || field.getType() == Character.class) {
value = ((String) value).charAt(0);
} else if (field.getType() == Date.class) {
long date = (Long) value;
if (date <= 0) {
value = null;
} else {
value = new Date(date);
}
}
putSetMethodValueByField((DataSupport) modelInstance, field, value);
}
}
}
if (foreignKeyAssociations != null) {
for (AssociationsInfo associationInfo : foreignKeyAssociations) {
String foreignKeyColumn = getForeignKeyColumnName(DBUtility
.getTableNameByClassName(associationInfo.getAssociatedClassName()));
int columnIndex = cursor.getColumnIndex(foreignKeyColumn);
if (columnIndex != -1) {
long associatedClassId = cursor.getLong(columnIndex);
try {
DataSupport associatedObj = (DataSupport) DataSupport.find(
Class.forName(associationInfo.getAssociatedClassName()),
associatedClassId);
if (associatedObj != null) {
putSetMethodValueByField((DataSupport) modelInstance,
associationInfo.getAssociateOtherModelFromSelf(), associatedObj);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
/**
* Get the foreign key associations of the specified class.
*
* @param className
* The full class name.
* @param isEager
* True to load the associated models, false not.
* @return The foreign key associations of the specified class
*/
protected List<AssociationsInfo> getForeignKeyAssociations(String className, boolean isEager) {
if (isEager) {
analyzeAssociations(className);
return fkInCurrentModel;
}
return null;
}
/**
* Get the types of parameters for {@link ContentValues#put}. Need two
* parameters. First is String type for key. Second is depend on field for
* value.
*
* @param field
* The field to get parameter type.
* @param fieldValue
* Value of the field. Only used to convert to String when the
* field is char.
* @param parameters
* If the field is char, convert the value to String at index 1.
* @return The types of parameters for {@link ContentValues#put}.
*/
private Class<?>[] getParameterTypes(Field field, Object fieldValue, Object[] parameters) {
Class<?>[] parameterTypes;
if (isCharType(field)) {
parameters[1] = String.valueOf(fieldValue);
parameterTypes = new Class[] { String.class, String.class };
} else {
if (field.getType().isPrimitive()) {
parameterTypes = new Class[] { String.class, getObjectType(field.getType()) };
} else if ("java.util.Date".equals(field.getType().getName())) {
parameterTypes = new Class[] { String.class, Long.class };
} else {
parameterTypes = new Class[] { String.class, field.getType() };
}
}
return parameterTypes;
}
/**
* Each primitive type has a corresponding object type. For example int and
* Integer, boolean and Boolean. This method gives a way to turn primitive
* type into object type.
*
* @param primitiveType
* The class of primitive type.
* @return If the passed in parameter is primitive type, return a
* corresponding object type. Otherwise return null.
*/
private Class<?> getObjectType(Class<?> primitiveType) {
if (primitiveType != null) {
if (primitiveType.isPrimitive()) {
String basicTypeName = primitiveType.getName();
if ("int".equals(basicTypeName)) {
return Integer.class;
} else if ("short".equals(basicTypeName)) {
return Short.class;
} else if ("long".equals(basicTypeName)) {
return Long.class;
} else if ("float".equals(basicTypeName)) {
return Float.class;
} else if ("double".equals(basicTypeName)) {
return Double.class;
} else if ("boolean".equals(basicTypeName)) {
return Boolean.class;
} else if ("char".equals(basicTypeName)) {
return Character.class;
}
}
}
return null;
}
/**
* Gives the passed in parameter an initialized value. If the parameter is
* basic data type or the corresponding object data type, return the default
* data. Or return null.
*
* @param paramType
* Parameter to get initialized value.
* @return Default data of basic data type or null.
*/
private Object getInitParamValue(Class<?> paramType) {
String paramTypeName = paramType.getName();
if ("boolean".equals(paramTypeName) || "java.lang.Boolean".equals(paramTypeName)) {
return false;
}
if ("float".equals(paramTypeName) || "java.lang.Float".equals(paramTypeName)) {
return 0f;
}
if ("double".equals(paramTypeName) || "java.lang.Double".equals(paramTypeName)) {
return 0.0;
}
if ("int".equals(paramTypeName) || "java.lang.Integer".equals(paramTypeName)) {
return 0;
}
if ("long".equals(paramTypeName) || "java.lang.Long".equals(paramTypeName)) {
return 0l;
}
if ("short".equals(paramTypeName) || "java.lang.Short".equals(paramTypeName)) {
return 0;
}
if ("char".equals(paramTypeName) || "java.lang.Character".equals(paramTypeName)) {
return ' ';
}
if ("java.lang.String".equals(paramTypeName)) {
return "";
}
return null;
}
/**
* Judge if the field is char or Character type.
*
* @param field
* Field to judge type.
* @return Return true if it's char or Character. Otherwise return false.
*/
private boolean isCharType(Field field) {
String type = field.getType().getName();
return type.equals("char") || type.endsWith("Character");
}
/**
* Judge a field is a primitive boolean type or not. Cause it's a little
* special when use IDE to generate getter and setter method. The primitive
* boolean type won't be like <b>getXxx</b>, it's something like
* <b>isXxx</b>.
*
* @param field
* Use field to get field type.
* @return If it's primitive boolean type return true, else return false.
*/
private boolean isPrimitiveBooleanType(Field field) {
Class<?> fieldType = field.getType();
if ("boolean".equals(fieldType.getName())) {
return true;
}
return false;
}
/**
* Put the value of field into ContentValues if current action is saving.
* Check the value of field is default value or not if current action is
* updating. If it's not default value, put it into ContentValues. Otherwise
* ignore it.
*
* @param baseObj
* Current model to persist or update.
* @param field
* With value to put into ContentValues.
* @param values
* To store data of current model for persisting or updating.
* @throws SecurityException
* @throws IllegalArgumentException
* @throws NoSuchMethodException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
private void putFieldsValueDependsOnSaveOrUpdate(DataSupport baseObj, Field field,
ContentValues values) throws SecurityException, IllegalArgumentException,
NoSuchMethodException, IllegalAccessException, InvocationTargetException {
if (isUpdating()) {
if (!isFieldWithDefaultValue(baseObj, field)) {
putContentValues(baseObj, field, values);
}
} else if (isSaving()) {
putContentValues(baseObj, field, values);
}
}
/**
* Current action is updating or not. Note that update the record by saving
* the already saved record again belongs to save action.
*
* @return If current action is updating return true. Otherwise return
* false.
*/
private boolean isUpdating() {
return UpdateHandler.class.getName().equals(getClass().getName());
}
/**
* Current action is saving or not. Note that update the record by saving
* the already saved record again belongs to save action.
*
* @return If current action is saving return true. Otherwise return false.
*/
private boolean isSaving() {
return SaveHandler.class.getName().equals(getClass().getName());
}
/**
* Analyze the passed in field. Judge if this field is with default value.
* The baseObj need a default constructor or {@link DataSupportException}
* will be thrown.
*
* @param baseObj
* Current model to update.
* @param field
* To judge if with default value.
* @return If the field is with default value, return true. Otherwise return
* false.
* @throws IllegalAccessException
* @throws SecurityException
* @throws IllegalArgumentException
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws DatabaseGenerateException
* @throws DataSupportException
*/
private boolean isFieldWithDefaultValue(DataSupport baseObj, Field field)
throws IllegalAccessException, SecurityException, IllegalArgumentException,
NoSuchMethodException, InvocationTargetException {
DataSupport emptyModel = getEmptyModel(baseObj);
Object realReturn = takeGetMethodValueByField(baseObj, field);
Object defaultReturn = takeGetMethodValueByField(emptyModel, field);
if (realReturn != null && defaultReturn != null) {
String realFieldValue = takeGetMethodValueByField(baseObj, field).toString();
String defaultFieldValue = takeGetMethodValueByField(emptyModel, field).toString();
return realFieldValue.equals(defaultFieldValue);
}
return realReturn == defaultReturn;
}
/**
* Generate the getter method name by field, following the eclipse rule.
*
* @param field
* The field to generate getter method from.
* @return The generated getter method name.
*/
private String makeGetterMethodName(Field field) {
String getterMethodPrefix;
String fieldName = field.getName();
if (isPrimitiveBooleanType(field)) {
if (fieldName.matches("^is[A-Z]{1}.*$")) {
fieldName = fieldName.substring(2);
}
getterMethodPrefix = "is";
} else {
getterMethodPrefix = "get";
}
if (fieldName.matches("^[a-z]{1}[A-Z]{1}.*")) {
return getterMethodPrefix + fieldName;
} else {
return getterMethodPrefix + BaseUtility.capitalize(fieldName);
}
}
/**
* Generate the setter method name by field, following the eclipse rule.
*
* @param field
* The field to generate setter method from.
* @return The generated setter method name.
*/
private String makeSetterMethodName(Field field) {
String setterMethodName;
String setterMethodPrefix = "set";
if (isPrimitiveBooleanType(field) && field.getName().matches("^is[A-Z]{1}.*$")) {
setterMethodName = setterMethodPrefix + field.getName().substring(2);
} else if (field.getName().matches("^[a-z]{1}[A-Z]{1}.*")) {
setterMethodName = setterMethodPrefix + field.getName();
} else {
setterMethodName = setterMethodPrefix + BaseUtility.capitalize(field.getName());
}
return setterMethodName;
}
/**
* Generates the getType method for cursor based on field. There're two
* unusual conditions. If field type is boolean, generate getInt method. If
* field type is char, generate getString method.
*
* @param field
* To generate getType method for cursor.
* @return The getType method for cursor.
*/
private String genGetColumnMethod(Field field) {
return genGetColumnMethod(field.getType());
}
/**
* Generates the getType method for cursor based on field. There're two
* unusual conditions. If field type is boolean, generate getInt method. If
* field type is char, generate getString method.
*
* @param fieldType
* To generate getType method for cursor.
* @return The getType method for cursor.
*/
private String genGetColumnMethod(Class<?> fieldType) {
String typeName;
if (fieldType.isPrimitive()) {
typeName = BaseUtility.capitalize(fieldType.getName());
} else {
typeName = fieldType.getSimpleName();
}
String methodName = "get" + typeName;
if ("getBoolean".equals(methodName)) {
methodName = "getInt";
} else if ("getChar".equals(methodName)) {
methodName = "getString";
} else if ("getDate".equals(methodName)) {
methodName = "getLong";
}
return methodName;
}
/**
* Customize the passed in columns. If the columns contains an id column
* already, just return it. If contains an _id column, rename it to id. If
* not, an add id column then return.
*
* @param columns
* The original columns that passed in.
* @param foreignKeyAssociations
* Associated classes which have foreign keys in the current
* model's table.
* @return Customized columns with id column always.
*/
private String[] getCustomizedColumns(String[] columns,
List<AssociationsInfo> foreignKeyAssociations) {
if (columns != null) {
if (foreignKeyAssociations != null && foreignKeyAssociations.size() > 0) {
String[] tempColumns = new String[columns.length + foreignKeyAssociations.size()];
System.arraycopy(columns, 0, tempColumns, 0, columns.length);
for (int i = 0; i < foreignKeyAssociations.size(); i++) {
String associatedTable = DBUtility
.getTableNameByClassName(foreignKeyAssociations.get(i)
.getAssociatedClassName());
tempColumns[columns.length + i] = getForeignKeyColumnName(associatedTable);
}
columns = tempColumns;
}
for (int i = 0; i < columns.length; i++) {
String columnName = columns[i];
if (isIdColumn(columnName)) {
if ("_id".equalsIgnoreCase(columnName)) {
columns[i] = BaseUtility.changeCase("id");
}
return columns;
}
}
String[] customizedColumns = new String[columns.length + 1];
System.arraycopy(columns, 0, customizedColumns, 0, columns.length);
customizedColumns[columns.length] = BaseUtility.changeCase("id");
return customizedColumns;
}
return null;
}
/**
* Analyze the associations for the specified class.
*
* @param className
* The full class name.
*/
private void analyzeAssociations(String className) {
Collection<AssociationsInfo> associationInfos = getAssociationInfo(className);
if (fkInCurrentModel == null) {
fkInCurrentModel = new ArrayList<AssociationsInfo>();
} else {
fkInCurrentModel.clear();
}
if (fkInOtherModel == null) {
fkInOtherModel = new ArrayList<AssociationsInfo>();
} else {
fkInOtherModel.clear();
}
for (AssociationsInfo associationInfo : associationInfos) {
if (associationInfo.getAssociationType() == Const.Model.MANY_TO_ONE
|| associationInfo.getAssociationType() == Const.Model.ONE_TO_ONE) {
if (associationInfo.getClassHoldsForeignKey().equals(className)) {
fkInCurrentModel.add(associationInfo);
} else {
fkInOtherModel.add(associationInfo);
}
} else if (associationInfo.getAssociationType() == Const.Model.MANY_TO_MANY) {
fkInOtherModel.add(associationInfo);
}
}
}
/**
* Finds the associated models of baseObj, then set them into baseObj.
*
* @param baseObj
* The class of base object.
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private void setAssociatedModel(DataSupport baseObj) {
if (fkInOtherModel == null) {
return;
}
for (AssociationsInfo info : fkInOtherModel) {
Cursor cursor = null;
String associatedClassName = info.getAssociatedClassName();
boolean isM2M = info.getAssociationType() == Const.Model.MANY_TO_MANY ? true : false;
try {
List<Field> supportedFields = getSupportedFields(associatedClassName);
if (isM2M) {
String tableName = baseObj.getTableName();
String associatedTableName = DBUtility
.getTableNameByClassName(associatedClassName);
String intermediateTableName = DBUtility.getIntermediateTableName(tableName,
associatedTableName);
StringBuilder sql = new StringBuilder();
sql.append("select * from ").append(associatedTableName)
.append(" a inner join ").append(intermediateTableName)
.append(" b on a.id = b.").append(associatedTableName + "_id")
.append(" where b.").append(tableName).append("_id = ?");
cursor = DataSupport.findBySQL(BaseUtility.changeCase(sql.toString()),
String.valueOf(baseObj.getBaseObjId()));
} else {
String foreignKeyColumn = getForeignKeyColumnName(DBUtility
.getTableNameByClassName(info.getSelfClassName()));
String associatedTableName = DBUtility
.getTableNameByClassName(associatedClassName);
cursor = mDatabase.query(BaseUtility.changeCase(associatedTableName), null,
foreignKeyColumn + "=?",
new String[] { String.valueOf(baseObj.getBaseObjId()) }, null, null,
null, null);
}
if (cursor.moveToFirst()) {
do {
Constructor<?> constructor = findBestSuitConstructor(Class
.forName(associatedClassName));
DataSupport modelInstance = (DataSupport) constructor
.newInstance(getConstructorParams(constructor));
giveBaseObjIdValue(modelInstance,
cursor.getLong(cursor.getColumnIndexOrThrow("id")));
setValueToModel(modelInstance, supportedFields, null, cursor);
if (info.getAssociationType() == Const.Model.MANY_TO_ONE || isM2M) {
Collection collection = (Collection) takeGetMethodValueByField(baseObj,
info.getAssociateOtherModelFromSelf());
collection.add(modelInstance);
} else if (info.getAssociationType() == Const.Model.ONE_TO_ONE) {
putSetMethodValueByField(baseObj,
info.getAssociateOtherModelFromSelf(), modelInstance);
}
} while (cursor.moveToNext());
}
} catch (Exception e) {
throw new DataSupportException(e.getMessage());
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
}