package org.kroz.activerecord; import java.lang.reflect.Field; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; /** * Base class for tables entities * * @author Vladimir Kroz * @author Wagied Davids * * <p> * This project based on and inspired by 'androidactiverecord' project * written by JEREMYOT * </p> */ public class ActiveRecordBase { static EntitiesMap s_EntitiesMap = new EntitiesMap(); boolean m_NeedsInsert = true; Database m_Database; /** * Creates new ActiveRecord instance. Returned instances is not initially * opened. Calling application must explicitly open it by calling open() * method * * @param db * @return */ static public ActiveRecordBase createInstance(Database db) { return new ActiveRecordBase(db); } /** * Creates and opens new ActiveRecord object instance and underlying * database. Returned ActiveRecord object is fully ready for use. * * @param ctx * @param dbName * @return * @throws ActiveRecordException */ static public ActiveRecordBase open(Context ctx, String dbName, int dbVersion) throws ActiveRecordException { Database db = Database.createInstance(ctx, dbName, dbVersion); db.open(); return ActiveRecordBase.createInstance(db); } /** * Opens ActiveRecord object and associated underlying database * * @throws ActiveRecordException */ public void open() throws ActiveRecordException { m_Database.open(); } /** * Returns true is underlying database object is open * * @return */ public boolean isOpen() { return m_Database.isOpen(); } /** * Closes ActiveRecord object and associated underlying database */ public void close() { m_Database.close(); } /** * * @param db */ protected ActiveRecordBase(Database db) { m_Database = db; } protected ActiveRecordBase() { } /** * Creates new entity instance connected with opened database * * @param <T> * @param type * The type of the required entity * @return New entity instance */ public <T extends ActiveRecordBase> T newEntity(Class<T> type) throws ActiveRecordException { T entity = null; try { entity = type.newInstance(); entity.setDatabase(m_Database); } catch (IllegalAccessException e) { throw new ActiveRecordException("Can't instantiate " + type.getClass()); } catch (InstantiationException e) { throw new ActiveRecordException("Can't instantiate " + type.getClass()); } return entity; } /** * Copies values of fields from src to current object. Scans src object for * the fields with the same names as in current object and copies it's * valies. All fields are copied except special fields: 'id', 'created', * 'modified', and also fields prefixed as 'm_*' and 's_' If src has fields * not defiend in current object such fields are ignored * * @param src */ public void copyFrom(Object src) { for (Field dstField : this.getColumnFieldsWithoutID()) { try { Field srcField = src.getClass().getField(dstField.getName()); dstField.set(this, srcField.get(src)); } catch (SecurityException e) { } catch (NoSuchFieldException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } } } /** * Call this once at application launch, sets the database to use for * AREntities. * * @param database * The database to use. */ public void setDatabase(Database database) { m_Database = database; } protected long _id = 0; /** * This entities row id. * * @return The SQLite row id. */ public long getID() { return _id; } /** * Get the table name for this class. * * @return The table name for this class. */ protected String getTableName() { return CamelNotationHelper.toSQLName(getClass().getSimpleName()); } /** * Get this class's columns without the id column. * * @return An array of the columns in this class's table. */ protected String[] getColumnsWithoutID() { List<String> columns = new ArrayList<String>(); for (Field field : getColumnFieldsWithoutID()) { columns.add(field.getName()); } return columns.toArray(new String[0]); } /** * Get this class's columns. * * @return An array of the columns in this class's table. */ protected String[] getColumns() { List<String> columns = new ArrayList<String>(); for (Field field : getColumnFields()) { columns.add(field.getName()); } return columns.toArray(new String[0]); } /** * Get this class's fields without id. * * @return An array of fields for this class. */ protected List<Field> getColumnFieldsWithoutID() { Field[] fields = getClass().getDeclaredFields(); List<Field> columns = new ArrayList<Field>(); for (Field field : fields) { if (!field.getName().startsWith("m_") && !field.getName().startsWith("s_")) columns.add(field); } return columns; } /** * Get this class's fields. * * @return An array of fields for this class. */ protected List<Field> getColumnFields() { Field[] fields = getClass().getDeclaredFields(); List<Field> columns = new ArrayList<Field>(); for (Field field : fields) { if (!field.getName().startsWith("m_") && !field.getName().startsWith("s_")) { columns.add(field); } } if (!getClass().equals(ActiveRecordBase.class)) { fields = ActiveRecordBase.class.getDeclaredFields(); for (Field field : fields) { if (!field.getName().startsWith("m_") && !field.getName().startsWith("s_")) { columns.add(field); } } } return columns; } /** * Insert this entity into the database. * * @return the row ID of the newly inserted row, or -1 if an error occurred * @throws ActiveRecordException */ public long insert() throws ActiveRecordException { List<Field> columns = _id > 0 ? getColumnFields() : getColumnFieldsWithoutID(); ContentValues values = new ContentValues(columns.size()); for (Field column : columns) { try { if (column.getType().getSuperclass() == ActiveRecordBase.class) values.put( CamelNotationHelper.toSQLName(column.getName()), column.get(this) != null ? String .valueOf(((ActiveRecordBase) column .get(this))._id) : "0"); else values.put(CamelNotationHelper.toSQLName(column.getName()), String.valueOf(column.get(this))); } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } } _id = m_Database.insert(getTableName(), values); if (-1 != _id) m_NeedsInsert = false; return _id; } /** * Update this entity in the database. * * @return The number of rows affected * @throws NoSuchFieldException */ public int update() throws ActiveRecordException { List<Field> columns = getColumnFieldsWithoutID(); ContentValues values = new ContentValues(columns.size()); for (Field column : columns) { try { if (column.getType().getSuperclass() == ActiveRecordBase.class) values.put( CamelNotationHelper.toSQLName(column.getName()), column.get(this) != null ? String .valueOf(((ActiveRecordBase) column .get(this))._id) : "0"); else values.put(CamelNotationHelper.toSQLName(column.getName()), String.valueOf(column.get(this))); } catch (IllegalArgumentException e) { throw new ActiveRecordException("No column " + column.getName()); } catch (IllegalAccessException e) { throw new ActiveRecordException("No column " + column.getName()); } } int r = m_Database.update(getTableName(), values, "_id = ?", new String[] { String.valueOf(_id) }); return r; } /** * Remove this entity from the database. * * @return Whether or the entity was successfully deleted. * @throws IllegalArgumentException * @throws IllegalAccessException */ public boolean delete() throws ActiveRecordException { if (m_Database == null) throw new ActiveRecordException("Set database first"); boolean toRet = m_Database.delete(getTableName(), "_id = ?", new String[] { String.valueOf(_id) }) != 0; _id = 0; m_NeedsInsert = true; return toRet; } /** * Saves this entity to the database, inserts or updates as needed. * * @return number of rows affected on success, -1 on failure * @throws ActiveRecordException */ public long save() throws ActiveRecordException { long r = -1; if (m_Database == null) throw new ActiveRecordException("Set database first"); if (null == findByID(this.getClass(), _id)) r = insert(); else r = update(); s_EntitiesMap.set(this); return r; } /** * Inflate this entity using the current row from the given cursor. * * @param cursor * The cursor to get object data from. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InstantiationException */ @SuppressWarnings("unchecked") void inflate(Cursor cursor) throws ActiveRecordException { HashMap<Field, Long> entities = new HashMap<Field, Long>(); for (Field field : getColumnFields()) { try { String typeString = field.getType().getName(); String colName = CamelNotationHelper.toSQLName(field.getName()); if (typeString.equals("long")) { field.setLong(this, cursor.getLong(cursor.getColumnIndex(colName))); } else if (typeString.equals("java.lang.String")) { String val = cursor.getString(cursor .getColumnIndex(colName)); field.set(this, val.equals("null") ? null : val); } else if (typeString.equals("double")) { field.setDouble(this, cursor.getDouble(cursor.getColumnIndex(colName))); } else if (typeString.equals("boolean")) { field.setBoolean(this, cursor.getString(cursor.getColumnIndex(colName)) .equals("true")); } else if (typeString.equals("[B")) { field.set(this, cursor.getBlob(cursor.getColumnIndex(colName))); } else if (typeString.equals("int")) { field.setInt(this, cursor.getInt(cursor.getColumnIndex(colName))); } else if (typeString.equals("float")) { field.setFloat(this, cursor.getFloat(cursor.getColumnIndex(colName))); } else if (typeString.equals("short")) { field.setShort(this, cursor.getShort(cursor.getColumnIndex(colName))); } else if (typeString.equals("java.sql.Timestamp")) { long l = cursor.getLong(cursor.getColumnIndex(colName)); field.set(this, new Timestamp(l)); } else if (field.getType().getSuperclass() == ActiveRecordBase.class) { long id = cursor.getLong(cursor.getColumnIndex(colName)); if (id > 0) entities.put(field, id); else field.set(this, null); } else throw new ActiveRecordException( "Class cannot be read from Sqlite3 database."); } catch (IllegalArgumentException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } } s_EntitiesMap.set(this); for (Field f : entities.keySet()) { try { f.set(this, this.findByID( (Class<? extends ActiveRecordBase>) f.getType(), entities.get(f))); } catch (SQLiteException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (IllegalArgumentException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } } } /** * Delete selected entities from the database. * * @param <T> * Any AREntity class. * @param type * The class of the entities to delete. * @param whereClause * The condition to match (Don't include "where"). * @param whereArgs * The arguments to replace "?" with. * @return The number of rows affected. * @throws IllegalAccessException * @throws InstantiationException */ public <T extends ActiveRecordBase> int delete(Class<T> type, String whereClause, String[] whereArgs) throws ActiveRecordException { if (m_Database == null) throw new ActiveRecordException("Set database first"); T entity; try { entity = type.newInstance(); } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (InstantiationException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } return m_Database.delete(entity.getTableName(), whereClause, whereArgs); } /** * Delete all instances of an entity from the database where a column has a * specified value. * * @param <T> * Any AREntity class. * @param type * The class of the entities to delete. * @param column * The column to match. * @param value * The value required for deletion. * @return The number of rows affected. * @throws ActiveRecordException */ public <T extends ActiveRecordBase> int deleteByColumn(Class<T> type, String column, String value) throws ActiveRecordException { return delete(type, String.format("%s = ?", column), new String[] { value }); } /** * Return all instances of an entity that match the given criteria. Use * whereClause to specify condition, using reqular SQL syntax for WHERE * clause. * <p> * For example selecting all JOHNs born in 2001 from USERS table may look like: * <pre> * users.find(Users.class, "NAME='?' and YEAR=?", new String[] {"John", "2001"}); * </pre> * * @param <T> * Any ActiveRecordBase class. * @param type * The class of the entities to return. * @param whereClause * The condition to match (Don't include "where"). * @param whereArgs * The arguments to replace "?" with. * @return A generic list of all matching entities. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InstantiationException */ public <T extends ActiveRecordBase> List<T> find(Class<T> type, String whereClause, String[] whereArgs) throws ActiveRecordException { if (m_Database == null) throw new ActiveRecordException("Set database first"); T entity = null; try { entity = type.newInstance(); } catch (IllegalAccessException e1) { throw new ActiveRecordException(e1.getLocalizedMessage()); } catch (InstantiationException e1) { throw new ActiveRecordException(e1.getLocalizedMessage()); } List<T> toRet = new ArrayList<T>(); Cursor c = m_Database.query(entity.getTableName(), null, whereClause, whereArgs); try { while (c.moveToNext()) { entity = s_EntitiesMap.get(type, c.getLong(c.getColumnIndex("_id"))); if (entity == null) { entity = type.newInstance(); entity.m_NeedsInsert = false; entity.inflate(c); entity.m_Database = m_Database; } toRet.add(entity); } } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (InstantiationException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } finally { c.close(); } return toRet; } /** * Return all instances of an entity that match the given criteria. * * @param <T> * Any ActiveRecordBase class. * @param type * The class of the entities to return. * @param distinct * @param whereClause * The condition to match (Don't include "where"). * @param whereArgs * The arguments to replace "?" with. * @param groupBy * @param having * @param orderBy * @param limit * @return A generic list of all matching entities. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InstantiationException */ public <T extends ActiveRecordBase> List<T> find(Class<T> type, boolean distinct, String whereClause, String[] whereArgs, String groupBy, String having, String orderBy, String limit) throws ActiveRecordException { if (m_Database == null) throw new ActiveRecordException("Set database first"); T entity; try { entity = type.newInstance(); } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (InstantiationException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } List<T> toRet = new ArrayList<T>(); Cursor c = m_Database.query(distinct, entity.getTableName(), null, whereClause, whereArgs, groupBy, having, orderBy, limit); try { while (c.moveToNext()) { entity = s_EntitiesMap.get(type, c.getLong(c.getColumnIndex("_id"))); if (entity == null) { entity = type.newInstance(); entity.m_NeedsInsert = false; entity.inflate(c); entity.m_Database = m_Database; } toRet.add(entity); } } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (InstantiationException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } finally { c.close(); } return toRet; } /** * Return all instances of an entity from the database where a column has a * specified value. * * @param <T> * Any ActiveRecordBase class. * @param type * The class of the entities to return. * @param column * The tables's column to match. Note - it must be name from DB * schema, not Java field name * @param value * The desired value. * @return A generic list of all matching entities. * @throws ActiveRecordException */ public <T extends ActiveRecordBase> List<T> findByColumn(Class<T> type, String column, String value) throws ActiveRecordException { return find(type, String.format("%s = ?", column), new String[] { value }); } /** * Return the instance of an entity with a matching id. * * @param <T> * Any ActiveRecordBase class. * @param type * The class of the entity to return. * @param id * The desired ID. * @return The matching entity if reocrd found in DB, null otherwise * @throws ActiveRecordException */ public <T extends ActiveRecordBase> T findByID(Class<T> type, long id) throws ActiveRecordException { if (m_Database == null) throw new ActiveRecordException("Set database first"); T entity = s_EntitiesMap.get(type, id); if (entity != null) return entity; try { entity = type.newInstance(); } catch (IllegalAccessException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } catch (InstantiationException e) { throw new ActiveRecordException(e.getLocalizedMessage()); } Cursor c = m_Database.query(entity.getTableName(), null, "_id = ?", new String[] { String.valueOf(id) }); try { if (!c.moveToNext()) { return null; } else { entity.inflate(c); entity.m_NeedsInsert = false; entity.m_Database = m_Database; } } finally { c.close(); } return entity; } /** * Return all instances of an entity from the database. * * @param <T> * Any ActiveRecordBase class. * @param type * The class of the entities to return. * @return A generic list of all matching entities. * @throws ActiveRecordException */ public <T extends ActiveRecordBase> List<T> findAll(Class<T> type) throws ActiveRecordException { return find(type, null, null); } /** * Returns underlying database object for direct manipulations * @return */ public Database getDatabase() { return m_Database; } }