/** * */ package by.istin.android.xcore.db.impl; import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; import android.provider.BaseColumns; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import java.util.Set; import by.istin.android.xcore.annotations.dbEntities; import by.istin.android.xcore.annotations.dbEntity; import by.istin.android.xcore.annotations.dbIndex; import by.istin.android.xcore.db.IDBConnection; import by.istin.android.xcore.db.IDBConnector; import by.istin.android.xcore.db.entity.IBeforeArrayUpdate; import by.istin.android.xcore.db.entity.IBeforeUpdate; import by.istin.android.xcore.db.entity.IGenerateID; import by.istin.android.xcore.db.entity.IMerge; import by.istin.android.xcore.source.DataSourceRequest; import by.istin.android.xcore.utils.BytesUtils; import by.istin.android.xcore.utils.CursorUtils; import by.istin.android.xcore.utils.Log; import by.istin.android.xcore.utils.ReflectUtils; import by.istin.android.xcore.utils.StringUtil; /** * @author Uladzimir_Klyshevich * */ public class DBHelper { private static final String TAG = DBHelper.class.getSimpleName(); private final IDBConnector mDbConnector; private final DBAssociationCache dbAssociationCache; public static final boolean IS_LOG_ENABLED = false; public DBHelper(IDBConnector dbConnector) { super(); mDbConnector = dbConnector; dbAssociationCache = DBAssociationCache.get(); } public static String getTableName(Class<?> clazz) { DBAssociationCache associationCache = DBAssociationCache.get(); String tableName = associationCache.getTableName(clazz); if (tableName == null) { tableName = clazz.getCanonicalName().replace(".", "_"); associationCache.setTableName(clazz, tableName); } return tableName; } public synchronized void createTablesForModels(Class<?>... models) { IDBConnection dbWriter = mDbConnector.getWritableConnection(); dbWriter.beginTransaction(); StringBuilder builder = new StringBuilder(); List<String> foreignKeys = new ArrayList<String>(); for (Class<?> classOfModel : models) { String table = getTableName(classOfModel); dbAssociationCache.setTableCreated(table, null); dbWriter.execSQL(mDbConnector.getCreateTableSQLTemplate(table)); Cursor columns = null; try { columns = dbWriter.query(table, null, null, null, null, null, null, "0,1"); List<ReflectUtils.XField> fields = ReflectUtils.getEntityKeys(classOfModel); for (ReflectUtils.XField field : fields) { try { String name = ReflectUtils.getStaticStringValue(field); if (name.equals(BaseColumns._ID)) { continue; } if (columns.getColumnIndex(name) != -1) { continue; } Annotation[] annotations = field.getField().getAnnotations(); String type = null; for (Annotation annotation : annotations) { Class<? extends Annotation> classOfAnnotation = annotation.annotationType(); if (DBAssociationCache.TYPE_ASSOCIATION.containsKey(classOfAnnotation)) { type = DBAssociationCache.TYPE_ASSOCIATION.get(classOfAnnotation); } else if (classOfAnnotation.equals(dbEntity.class)) { List<ReflectUtils.XField> list = dbAssociationCache.getEntityFields(classOfModel); if (list == null) { list = new ArrayList<ReflectUtils.XField>(); } list.add(field); dbAssociationCache.putEntityFields(classOfModel, list); } else if (classOfAnnotation.equals(dbEntities.class)) { List<ReflectUtils.XField> list = dbAssociationCache.getEntitiesFields(classOfModel); if (list == null) { list = new ArrayList<ReflectUtils.XField>(); } list.add(field); dbAssociationCache.putEntitiesFields(classOfModel, list); } else if (classOfAnnotation.equals(dbIndex.class)) { builder.append(mDbConnector.getCreateIndexSQLTemplate(table, name)); } } if (type == null) { continue; } dbWriter.execSQL(mDbConnector.getCreateColumnSQLTemplate(table, name, type)); } catch (SQLException e) { if (IS_LOG_ENABLED) Log.w(TAG, e); } } } finally { CursorUtils.close(columns); } String sql = builder.toString(); Log.xd(this, sql); if (!StringUtil.isEmpty(sql)) { try { dbWriter.execSQL(sql); } catch (SQLException e) { if (IS_LOG_ENABLED) Log.w(TAG, e); } } builder.setLength(0); } setTransactionSuccessful(dbWriter); endTransaction(dbWriter); } public int delete(Class<?> clazz, String where, String[] whereArgs) { return delete(null, getTableName(clazz), where, whereArgs); } public int delete(IDBConnection db, Class<?> clazz, String where, String[] whereArgs) { return delete(db, getTableName(clazz), where, whereArgs); } public int delete(String tableName, String where, String[] whereArgs) { return delete(null, tableName, where, whereArgs); } public int delete(IDBConnection db, String tableName, String where, String[] whereArgs) { if (isExists(tableName)) { if (db == null) { db = mDbConnector.getWritableConnection(); } return db.delete(tableName, where, whereArgs); } else { return 0; } } public boolean isExists(String tableName) { Boolean isTableCreated = dbAssociationCache.isTableCreated(tableName); if (isTableCreated != null) { return isTableCreated; } IDBConnection readableDb = mDbConnector.getReadableConnection(); boolean isExists = readableDb.isExists(tableName); dbAssociationCache.setTableCreated(tableName, isExists); return isExists; } public int updateOrInsert(Class<?> classOfModel, ContentValues... contentValues) { return updateOrInsert(null, classOfModel, contentValues); } public int updateOrInsert(DataSourceRequest dataSourceRequest, Class<?> classOfModel, ContentValues... contentValues) { if (contentValues == null) { return 0; } IDBConnection db = mDbConnector.getWritableConnection(); try { beginTransaction(db); int count = updateOrInsert(dataSourceRequest, classOfModel, db, contentValues); setTransactionSuccessful(db); return count; } finally { endTransaction(db); } } public int updateOrInsert(DataSourceRequest dataSourceRequest, Class<?> classOfModel, IDBConnection db, ContentValues[] contentValues) { IBeforeArrayUpdate beforeListUpdate = ReflectUtils.getInstanceInterface(classOfModel, IBeforeArrayUpdate.class); int count = 0; for (int i = 0; i < contentValues.length; i++) { ContentValues contentValue = contentValues[i]; if (contentValue == null) { continue; } if (beforeListUpdate != null) { beforeListUpdate.onBeforeListUpdate(this, db, dataSourceRequest, i, contentValue); } long id = updateOrInsert(dataSourceRequest, db, classOfModel, contentValue); if (id != -1l) { count++; } } return count; } public long updateOrInsert(DataSourceRequest dataSourceRequest, IDBConnection db, Class<?> classOfModel, ContentValues contentValues) { boolean requestWithoutTransaction = false; if (db == null) { db = mDbConnector.getWritableConnection(); requestWithoutTransaction = true; beginTransaction(db); } try { IBeforeUpdate beforeUpdate = ReflectUtils.getInstanceInterface(classOfModel, IBeforeUpdate.class); if (beforeUpdate != null) { beforeUpdate.onBeforeUpdate(this, db, dataSourceRequest, contentValues); } String idAsString = contentValues.getAsString(BaseColumns._ID); Long id = null; if (idAsString == null) { IGenerateID generateId = ReflectUtils.getInstanceInterface(classOfModel, IGenerateID.class); if (generateId != null) { id = generateId.generateId(this, db, dataSourceRequest, contentValues); contentValues.put(BaseColumns._ID, id); } if (id == null) { Log.xd(this, "error to insert ContentValues["+classOfModel+"]: " + contentValues.toString()); throw new IllegalArgumentException("content values needs to contains _ID. Details: " + "error to insert ContentValues["+classOfModel+"]: " + contentValues.toString()); } } else { id = Long.valueOf(idAsString); } List<ReflectUtils.XField> listDbEntity = dbAssociationCache.getEntityFields(classOfModel); if (listDbEntity != null) { storeSubEntity(dataSourceRequest, id, classOfModel, db, contentValues, dbEntity.class, listDbEntity); } List<ReflectUtils.XField> listDbEntities = dbAssociationCache.getEntitiesFields(classOfModel); if (listDbEntities != null) { storeSubEntity(dataSourceRequest, id, classOfModel, db, contentValues, dbEntities.class, listDbEntities); } String tableName = getTableName(classOfModel); IMerge merge = ReflectUtils.getInstanceInterface(classOfModel, IMerge.class); long rowId = 0; if (merge == null) { int rowCount = db.update(tableName, contentValues, BaseColumns._ID + " = ?", new String[]{String.valueOf(id)}); if (rowCount == 0) { rowId = internalInsert(db, contentValues, tableName); if (rowId == -1l) { throw new IllegalArgumentException("can not insert content values:" + contentValues.toString() + " to table " + classOfModel+". Check keys in contentvalues and fields in model."); } } else { rowId = id; } } else { Cursor cursor = null; try { cursor = query(classOfModel, null, BaseColumns._ID + " = ?", new String[]{String.valueOf(id)}, null, null, null, null); if (cursor == null || !cursor.moveToFirst()) { rowId = internalInsert(db, contentValues, tableName); if (rowId == -1l) { throw new IllegalArgumentException("can not insert content values:" + contentValues.toString() + " to table " + classOfModel+". Check keys in contentvalues and fields in model."); } } else { ContentValues oldContentValues = new ContentValues(); CursorUtils.cursorRowToContentValues(classOfModel, cursor, oldContentValues); merge.merge(this, db, dataSourceRequest, oldContentValues, contentValues); if (!isContentValuesEquals(oldContentValues, contentValues)) { internalUpdate(db, contentValues, id, tableName); rowId = id; } else { rowId = -1l; } } } finally { CursorUtils.close(cursor); } } if (requestWithoutTransaction) { setTransactionSuccessful(db); } return rowId; } finally { if (requestWithoutTransaction) { endTransaction(db); } } } private int internalUpdate(IDBConnection db, ContentValues contentValues, Long id, String tableName) { return db.update(tableName, contentValues, BaseColumns._ID + " = " + id, null); } public void endTransaction(IDBConnection dbWriter) { dbWriter.endTransaction(); } public void setTransactionSuccessful(IDBConnection dbWriter) { dbWriter.setTransactionSuccessful(); } public void beginTransaction(IDBConnection dbWriter) { dbWriter.beginTransaction(); } private long internalInsert(IDBConnection db, ContentValues contentValues, String tableName) { return db.insert(tableName, contentValues); } public static boolean isContentValuesEquals(ContentValues oldContentValues, ContentValues contentValues) { Set<Entry<String, Object>> keySet = contentValues.valueSet(); for (Entry<String, Object> entry : keySet) { Object newObject = entry.getValue(); Object oldObject = oldContentValues.get(entry.getKey()); if (newObject == null && oldObject == null) { continue; } if (newObject != null && newObject.equals(oldObject)) { } else { return false; } } return true; } private void storeSubEntity(DataSourceRequest dataSourceRequest, long id, Class<?> foreignEntity, IDBConnection db, ContentValues contentValues, Class<? extends Annotation> dbAnnotation, List<ReflectUtils.XField> listDbEntity) { for (ReflectUtils.XField field : listDbEntity) { String columnName = ReflectUtils.getStaticStringValue(field); byte[] entityAsByteArray = contentValues.getAsByteArray(columnName); if (entityAsByteArray == null) { continue; } Annotation annotation = ReflectUtils.getAnnotation(field, dbAnnotation); String contentValuesKey; String foreignId = getForeignKey(foreignEntity); try { contentValuesKey = (String) annotation.annotationType().getMethod("contentValuesKey").invoke(annotation); } catch (Exception e) { throw new IllegalArgumentException(e); } String className = contentValues.getAsString(contentValuesKey); Class<?> modelClass; try { modelClass = Class.forName(className); } catch (ClassNotFoundException e1) { throw new IllegalArgumentException(e1); } if (annotation.annotationType().equals(dbEntity.class)) { ContentValues entityValues = BytesUtils.contentValuesFromByteArray(entityAsByteArray); putForeignIdAndClear(id, contentValuesKey, foreignId, entityValues); updateOrInsert(dataSourceRequest, db, modelClass, entityValues); } else { ContentValues[] entitiesValues = BytesUtils.arrayContentValuesFromByteArray(entityAsByteArray); for (ContentValues cv : entitiesValues) { putForeignIdAndClear(id, contentValuesKey, foreignId, cv); } updateOrInsert(dataSourceRequest, modelClass, db, entitiesValues); } contentValues.remove(columnName); contentValues.remove(contentValuesKey); } } public static String getForeignKey(Class<?> foreignEntity) { DBAssociationCache associationCache = DBAssociationCache.get(); String foreignKey = associationCache.getForeignKey(foreignEntity); if (foreignKey == null) { foreignKey = foreignEntity.getSimpleName().toLowerCase()+"_id"; associationCache.putForeignKey(foreignEntity, foreignKey); return foreignKey; } return foreignKey; } private void putForeignIdAndClear(long id, String contentValuesKey, String foreignId, ContentValues entityValues) { entityValues.remove(contentValuesKey); entityValues.put(foreignId, id); } public Cursor query(Class<?> clazz, String[] projection, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit) { return query(getTableName(clazz), projection, selection, selectionArgs, groupBy, having, sortOrder, limit); } public Cursor query(String tableName, String[] projection, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit) { if (isExists(tableName)) { IDBConnection db = mDbConnector.getReadableConnection(); return db.query(tableName, projection, selection, selectionArgs, groupBy, having, sortOrder, limit); } else { return null; } } public Cursor rawQuery(String sql, String[] selectionArgs) { IDBConnection db = mDbConnector.getReadableConnection(); return db.rawQuery(sql, selectionArgs); } public static void moveFromOldValues(ContentValues oldValues, ContentValues newValues, String ... keys) { for (String key : keys) { Object value = oldValues.get(key); if (value != null && newValues.get(key) == null) { if (value instanceof Long) { newValues.put(key, (Long)value); } else if (value instanceof Integer) { newValues.put(key, (Integer)value); } else if (value instanceof String) { newValues.put(key, (String)value); } else if (value instanceof Byte) { newValues.put(key, (Byte)value); } else if (value instanceof byte[]) { newValues.put(key, (byte[])value); } else if (value instanceof Boolean) { newValues.put(key, (Boolean)value); } else if (value instanceof Double) { newValues.put(key, (Double)value); } else if (value instanceof Float) { newValues.put(key, (Float)value); } else if (value instanceof Short) { newValues.put(key, (Short)value); } } } } public static ContentValues duplicateContentValues(ContentValues contentValues) { ContentValues values = new ContentValues(); Set<Entry<String, Object>> entries = contentValues.valueSet(); for (Entry<String, Object> keyValue : entries) { values.put(keyValue.getKey(), String.valueOf(keyValue.getValue())); } return values; } public IDBConnection getWritableDbConnection() { return mDbConnector.getWritableConnection(); } }