/* * Copyright (c) 2009, Todoroo Inc * All Rights Reserved * http://www.todoroo.com */ package com.todoroo.andlib.data; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import com.todoroo.andlib.data.Property.PropertyVisitor; import com.todoroo.andlib.service.ContextManager; /** * AbstractDatabase is a database abstraction which wraps a SQLite database. *

* Users of this class are in charge of the database's lifecycle - ensuring that * the database is open when needed and closed when usage is finished. Within an * activity, this is typically accomplished through the onResume and onPause * methods, though if the database is not needed for the activity's entire * lifecycle, it can be closed earlier. *

* Direct querying is not recommended for type safety reasons. Instead, use one * of the service classes to issue the request and return a {@link TodorooCursor}. * * @author Tim Su * */ @SuppressWarnings("nls") abstract public class AbstractDatabase { // --- abstract methods /** * @return database name */ protected abstract String getName(); /** * @return all tables in this database */ protected abstract Table[] getTables(); /** * @return database version */ protected abstract int getVersion(); /** * Called after database and tables are created. Use this method to * create indices and perform other database maintenance */ protected abstract void onCreateTables(); /** * Upgrades an open database from one version to the next * @param oldVersion * @param newVersion * @return true if upgrade was handled, false otherwise */ protected abstract boolean onUpgrade(int oldVersion, int newVersion); // --- protected variables /** * SQLiteOpenHelper that takes care of database operations */ protected SQLiteOpenHelper helper = null; /** * Internal pointer to open database. Hides the fact that there is a * database and a wrapper by making a single monolithic interface */ protected SQLiteDatabase database = null; // --- internal implementation /** * Return the name of the table containing these models * @param modelType * @return */ public final Table getTable(Class modelType) { for(Table table : getTables()) { if(table.modelClass.equals(modelType)) return table; } throw new UnsupportedOperationException("Unknown model class " + modelType); //$NON-NLS-1$ } protected synchronized final void initializeHelper() { if(helper == null) helper = new DatabaseHelper(ContextManager.getContext(), getName(), null, getVersion()); } /** * Open the database for writing. Must be closed afterwards. If user is * out of disk space, database may be opened for reading instead */ public synchronized final void openForWriting() { initializeHelper(); if(database != null && !database.isReadOnly() && database.isOpen()) return; try { database = helper.getWritableDatabase(); } catch (SQLiteException writeException) { Log.e("database-" + getName(), "Error opening db", writeException); try { // provide read-only database openForReading(); } catch (SQLiteException readException) { // throw original write exception throw writeException; } } } /** * Open the database for reading. Must be closed afterwards */ public synchronized final void openForReading() { initializeHelper(); if(database != null && database.isOpen()) return; database = helper.getReadableDatabase(); } /** * Close the database if it has been opened previously */ public synchronized final void close() { if(database != null) { database.close(); } database = null; } /** * Clear all data in database. Warning: this does what it says. Any open * database resources will be abruptly closed. */ public synchronized final void clear() { close(); ContextManager.getContext().deleteDatabase(getName()); } /** * @return sql database. opens database if not yet open */ private synchronized final SQLiteDatabase getDatabase() { return database; } /** * @return human-readable database name for debugging */ @Override public String toString() { return "DB:" + getName(); } // --- database wrapper /* * @see android.database.sqlite.SQLiteDatabase#rawQuery(String sql, String[] selectionArgs) */ public synchronized Cursor rawQuery(String sql, String[] selectionArgs) { return getDatabase().rawQuery(sql, selectionArgs); } /* * @see android.database.sqlite.SQLiteDatabase#insert(String table, String nullColumnHack, ContentValues values) */ public synchronized long insert(String table, String nullColumnHack, ContentValues values) { return getDatabase().insert(table, nullColumnHack, values); } /* * @see android.database.sqlite.SQLiteDatabase#delete(String table, String whereClause, String[] whereArgs) */ public synchronized int delete(String table, String whereClause, String[] whereArgs) { return getDatabase().delete(table, whereClause, whereArgs); } /* * @see android.database.sqlite.SQLiteDatabase#update(String table, ContentValues values, String whereClause, String[] whereArgs) */ public synchronized int update(String table, ContentValues values, String whereClause, String[] whereArgs) { return getDatabase().update(table, values, whereClause, whereArgs); } // --- helper classes /** * Default implementation of Astrid database helper */ private class DatabaseHelper extends SQLiteOpenHelper { public DatabaseHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } /** * Called to create the database tables */ @Override public synchronized void onCreate(SQLiteDatabase db) { StringBuilder sql = new StringBuilder(); SqlConstructorVisitor sqlVisitor = new SqlConstructorVisitor(); // create tables for(Table table : getTables()) { sql.append("CREATE TABLE IF NOT EXISTS ").append(table.name).append('('). append(AbstractModel.ID_PROPERTY).append(" INTEGER PRIMARY KEY AUTOINCREMENT"); for(Property property : table.getProperties()) { if(AbstractModel.ID_PROPERTY.name.equals(property.name)) continue; sql.append(',').append(property.accept(sqlVisitor, null)); } sql.append(')'); db.execSQL(sql.toString()); sql.setLength(0); } // post-table-creation database = db; onCreateTables(); } /** * Called to upgrade the database to a new version */ @Override public synchronized void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w("database-" + getName(), String.format("Upgrading database from version %d to %d.", oldVersion, newVersion)); database = db; if(!AbstractDatabase.this.onUpgrade(oldVersion, newVersion)) { // We don't know how to handle this case because someone forgot to // implement the upgrade. We can't drop tables, we can only // throw a nasty exception at this time throw new IllegalStateException("Missing database migration " + "from " + oldVersion + " to " + newVersion); } } } /** * Visitor that returns SQL constructor for this property * * @author Tim Su * */ public static class SqlConstructorVisitor implements PropertyVisitor { public String visitDouble(Property property, Void data) { return String.format("%s REAL", property.name); } public String visitInteger(Property property, Void data) { return String.format("%s INTEGER", property.name); } public String visitLong(Property property, Void data) { return String.format("%s INTEGER", property.name); } public String visitString(Property property, Void data) { return String.format("%s TEXT", property.name); } } }