/*
* 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 java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.litepal.crud.model.AssociationsInfo;
import org.litepal.exceptions.DataSupportException;
import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
/**
* This is a component under DataSupport. It deals with the saving stuff as
* primary task. All the implementation based on the java reflection API and
* Android SQLiteDatabase API. It will persist the model class into table. If
* there're some associated models already persisted, it will build the
* associations in database automatically between the current model and the
* associated models.
*
* @author Tony Green
* @since 1.1
*/
class SaveHandler extends DataHandler {
/**
* Initialize {@link DataHandler#mDatabase} for operating database. Do not
* allow to create instance of SaveHandler out of CRUD package.
*
* @param db
* The instance of SQLiteDatabase.
*/
SaveHandler(SQLiteDatabase db) {
mDatabase = db;
}
/**
* The open interface for other classes in CRUD package to save a model. It
* is called when a model class calls the save method. First of all, the
* passed in baseObj will be saved into database. Then LitePal will analyze
* the associations. If there're associated models detected, each associated
* model which is persisted will build association with current model in
* database.
*
* @param baseObj
* Current model to persist.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws SecurityException
*/
void onSave(DataSupport baseObj) throws SecurityException, IllegalArgumentException,
NoSuchMethodException, IllegalAccessException, InvocationTargetException {
String className = baseObj.getClassName();
List<Field> supportedFields = getSupportedFields(className);
Collection<AssociationsInfo> associationInfos = getAssociationInfo(className);
if (!baseObj.isSaved()) {
analyzeAssociatedModels(baseObj, associationInfos);
doSaveAction(baseObj, supportedFields);
analyzeAssociatedModels(baseObj, associationInfos);
} else {
analyzeAssociatedModels(baseObj, associationInfos);
doUpdateAction(baseObj, supportedFields);
}
}
/**
* The open interface for other classes in CRUD package to save a model
* collection. It is called when developer calls
* {@link DataSupport#saveAll(Collection)}. Each model in the collection
* will be persisted. If there're associated models detected, each
* associated model which is persisted will build association with current
* model in database.
*
* @param collection
* Holds all models to persist.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws SecurityException
*/
<T extends DataSupport> void onSaveAll(Collection<T> collection) throws SecurityException,
IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
if (collection != null && collection.size() > 0) {
DataSupport[] array = collection.toArray(new DataSupport[0]);
DataSupport firstObj = array[0];
String className = firstObj.getClassName();
List<Field> supportedFields = getSupportedFields(className);
Collection<AssociationsInfo> associationInfos = getAssociationInfo(className);
for (DataSupport baseObj : array) {
if (!baseObj.isSaved()) {
analyzeAssociatedModels(baseObj, associationInfos);
doSaveAction(baseObj, supportedFields);
analyzeAssociatedModels(baseObj, associationInfos);
} else {
analyzeAssociatedModels(baseObj, associationInfos);
doUpdateAction(baseObj, supportedFields);
}
baseObj.clearAssociatedData();
}
}
}
/**
* Persisting model class into database happens here. The ultimate way to
* save data calls {@link #saveAssociatedModels(DataSupport)}. But first
* {@link #beforeSave(DataSupport, List, ContentValues)} will be called to
* put the values for ContentValues. When the model is saved,
* {@link #afterSave(DataSupport, List, long)} will be called to do stuffs
* after model is saved. Note that SaveSupport won't help with id. Any
* developer who wants to set value to id will be ignored here. The value of
* id will be generated by SQLite automatically.
*
* @param baseObj
* Current model to persist.
* @param supportedFields
* List of all supported fields.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws SecurityException
*/
private void doSaveAction(DataSupport baseObj, List<Field> supportedFields)
throws SecurityException, IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
ContentValues values = new ContentValues();
beforeSave(baseObj, supportedFields, values);
long id = saving(baseObj, values);
afterSave(baseObj, supportedFields, id);
}
/**
* Before the self model is saved, it will be analyzed first. Put all the
* data contained by the model into ContentValues, including the fields
* value and foreign key value.
*
* @param baseObj
* Current model to persist.
* @param supportedFields
* List of all supported fields.
* @param values
* To store data of current model for persisting.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws SecurityException
*/
private void beforeSave(DataSupport baseObj, List<Field> supportedFields, ContentValues values)
throws SecurityException, IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
putFieldsValue(baseObj, supportedFields, values);
putForeignKeyValue(values, baseObj);
}
/**
* Calling {@link SQLiteDatabase#insert(String, String, ContentValues)} to
* persist the current model.
*
* @param baseObj
* Current model to persist.
* @param values
* To store data of current model for persisting.
* @return The row ID of the newly inserted row, or -1 if an error occurred.
*/
private long saving(DataSupport baseObj, ContentValues values) {
return mDatabase.insert(baseObj.getTableName(), null, values);
}
/**
* After the model is saved, do the extra work that need to do.
*
* @param baseObj
* Current model that is persisted.
* @param supportedFields
* List of all supported fields.
* @param id
* The current model's id.
*/
private void afterSave(DataSupport baseObj, List<Field> supportedFields, long id) {
throwIfSaveFailed(id);
assignIdValue(baseObj, getIdField(supportedFields), id);
updateAssociatedTableWithFK(baseObj);
insertIntermediateJoinTableValue(baseObj, false);
}
/**
* When a model is associated with two different models.
*
* @param baseObj
* The class of base object.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws SecurityException
*/
private void doUpdateAction(DataSupport baseObj, List<Field> supportedFields)
throws SecurityException, IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
ContentValues values = new ContentValues();
beforeUpdate(baseObj, supportedFields, values);
updating(baseObj, values);
afterUpdate(baseObj);
}
/**
* Before updating model, it will be analyzed first. Put all the data
* contained by the model into ContentValues, including the fields value and
* foreign key value. If the associations between models has been removed.
* The foreign key value in database should be cleared too.
*
* @param baseObj
* Current model to update.
* @param supportedFields
* List of all supported fields.
* @param values
* To store data of current model for updating.
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IllegalArgumentException
* @throws SecurityException
*/
private void beforeUpdate(DataSupport baseObj, List<Field> supportedFields, ContentValues values)
throws SecurityException, IllegalArgumentException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
putFieldsValue(baseObj, supportedFields, values);
putForeignKeyValue(values, baseObj);
for (String fkName : baseObj.getListToClearSelfFK()) {
values.putNull(fkName);
}
}
/**
* Calling
* {@link SQLiteDatabase#update(String, ContentValues, String, String[])} to
* update the current model.
*
* @param baseObj
* Current model to update.
* @param values
* To store data of current model for updating.
*/
private void updating(DataSupport baseObj, ContentValues values) {
mDatabase.update(baseObj.getTableName(), values, "id = ?",
new String[] { String.valueOf(baseObj.getBaseObjId()) });
}
/**
* After the model is updated, do the extra work that need to do.
*
* @param baseObj
* Current model that is updated.
*/
private void afterUpdate(DataSupport baseObj) {
updateAssociatedTableWithFK(baseObj);
insertIntermediateJoinTableValue(baseObj, true);
clearFKValueInAssociatedTable(baseObj);
}
/**
* Get the id field by the passed in field list.
*
* @param supportedFields
* The field list to find from.
* @return The id field. If not found one return null.
*/
private Field getIdField(List<Field> supportedFields) {
for (Field field : supportedFields) {
if (isIdColumn(field.getName())) {
return field;
}
}
return null;
}
/**
* If the model saved failed, throw an exception.
*
* @param id
* The id returned by SQLite. -1 means saved failed.
*/
private void throwIfSaveFailed(long id) {
if (id == -1) {
throw new DataSupportException(DataSupportException.SAVE_FAILED);
}
}
/**
* Assign the generated id value to the model. The
* {@link DataSupport#baseObjId} will be assigned anyway. If the model has a
* field named id or _id, LitePal will assign it too. The
* {@link DataSupport#baseObjId} will be used as identify of this model for
* system use. The id or _id field will help developers for their own
* purpose.
*
* @param baseObj
* Current model that is persisted.
* @param idField
* The field of id.
* @param id
* The value of id.
*/
private void assignIdValue(DataSupport baseObj, Field idField, long id) {
try {
giveBaseObjIdValue(baseObj, id);
if (idField != null) {
giveModelIdValue(baseObj, idField.getName(), idField.getType(), id);
}
} catch (Exception e) {
throw new DataSupportException(e.getMessage());
}
}
/**
* After saving a model, the id for this model will be returned. Assign this
* id to the model's id or _id field if it exists.
*
* @param baseObj
* The class of base object.
* @param idName
* The name of id. Only id or _id is valid.
* @param idType
* The type of id. Only int or long is valid.
* @param id
* The value of id.
* @throws SecurityException
* @throws NoSuchFieldException
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private void giveModelIdValue(DataSupport baseObj, String idName, Class<?> idType, long id)
throws SecurityException, NoSuchFieldException, IllegalArgumentException,
IllegalAccessException {
if (shouldGiveModelIdValue(idName, idType, id)) {
Object value = null;
if (idType == int.class || idType == Integer.class) {
value = (int) id;
} else if (idType == long.class || idType == Long.class) {
value = id;
} else {
throw new DataSupportException(DataSupportException.ID_TYPE_INVALID_EXCEPTION);
}
DynamicExecutor.setField(baseObj, idName, value, baseObj.getClass());
}
}
/**
* If the table for this model have a foreign key column, the value of
* foreign key id should be saved too.
*
* @param values
* The instance of ContentValues to put foreign key value.
* @param foreignKeyName
* Foreign key column name.
* @param foreignKeyValue
* Foreign key column value.
*/
private void putForeignKeyValue(ContentValues values, DataSupport baseObj) {
Map<String, Long> associatedModelMap = baseObj.getAssociatedModelsMapWithoutFK();
for (String associatedTableName : associatedModelMap.keySet()) {
values.put(getForeignKeyColumnName(associatedTableName),
associatedModelMap.get(associatedTableName));
}
}
/**
* Update the foreign keys in the associated model's table.
*
* @param baseObj
* Current model that is persisted.
*/
private void updateAssociatedTableWithFK(DataSupport baseObj) {
Map<String, Set<Long>> associatedModelMap = baseObj.getAssociatedModelsMapWithFK();
ContentValues values = new ContentValues();
for (String associatedTableName : associatedModelMap.keySet()) {
values.clear();
String fkName = getForeignKeyColumnName(baseObj.getTableName());
values.put(fkName, baseObj.getBaseObjId());
Set<Long> ids = associatedModelMap.get(associatedTableName);
if (ids != null && !ids.isEmpty()) {
mDatabase.update(associatedTableName, values, getWhereOfIdsWithOr(ids), null);
}
}
}
/**
* When the associations breaks between current model and associated models,
* clear all the associated models' foreign key value if it exists.
*
* @param baseObj
* Current model that is persisted.
*/
private void clearFKValueInAssociatedTable(DataSupport baseObj) {
List<String> associatedTableNames = baseObj.getListToClearAssociatedFK();
for (String associatedTableName : associatedTableNames) {
String fkColumnName = getForeignKeyColumnName(baseObj.getTableName());
ContentValues values = new ContentValues();
values.putNull(fkColumnName);
String whereClause = fkColumnName + " = " + baseObj.getBaseObjId();
mDatabase.update(associatedTableName, values, whereClause, null);
}
}
/**
* Insert values into intermediate join tables for self model and associated
* models.
*
* @param baseObj
* Current model that is persisted.
* @param isUpdate
* The current action is update or not.
*/
private void insertIntermediateJoinTableValue(DataSupport baseObj, boolean isUpdate) {
Map<String, Set<Long>> associatedIdsM2M = baseObj.getAssociatedModelsMapForJoinTable();
ContentValues values = new ContentValues();
for (String associatedTableName : associatedIdsM2M.keySet()) {
String joinTableName = getIntermediateTableName(baseObj, associatedTableName);
if (isUpdate) {
mDatabase.delete(joinTableName, getWhereForJoinTableToDelete(baseObj),
new String[] { String.valueOf(baseObj.getBaseObjId()) });
}
Set<Long> associatedIdsM2MSet = associatedIdsM2M.get(associatedTableName);
for (long associatedId : associatedIdsM2MSet) {
values.clear();
values.put(getForeignKeyColumnName(baseObj.getTableName()), baseObj.getBaseObjId());
values.put(getForeignKeyColumnName(associatedTableName), associatedId);
mDatabase.insert(joinTableName, null, values);
}
}
}
/**
* Get the where clause to delete intermediate join table's value for
* updating.
*
* @param baseObj
* Current model that is persisted.
* @return The where clause to execute.
*/
private String getWhereForJoinTableToDelete(DataSupport baseObj) {
StringBuilder where = new StringBuilder();
where.append(getForeignKeyColumnName(baseObj.getTableName()));
where.append(" = ?");
return where.toString();
}
/**
* Judge should assign id value to model's id field. The principle is that
* if id name is not null, id type is not null and id is greater than 0,
* then should assign id value to it.
*
* @param idName
* The name of id field.
* @param idType
* The type of id field.
* @param id
* The value of id.
* @return If id name is not null, id type is not null and id is greater
* than 0, return true. Otherwise return false.
*/
private boolean shouldGiveModelIdValue(String idName, Class<?> idType, long id) {
return idName != null && idType != null && id > 0;
}
}