package com.thebluealliance.androidclient.database;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
/**
* Base class for typed storage of models in the database
* @param <T> Type of the corresponding model, must implement {@link TbaDatabaseModel}
*/
public abstract class ModelTable<T extends TbaDatabaseModel> {
protected final SQLiteDatabase mDb;
protected final Gson mGson;
public ModelTable(SQLiteDatabase db, Gson gson){
mDb = db;
mGson = gson;
}
/**
* Adds a model to the database, if it doesn't already exist
* If the model is already entered, update the existing row via
* {@link #update(TbaDatabaseModel, Long)}
* If the insert was successful, call {@link #insertCallback(TbaDatabaseModel)}
* @param in Model to be added
* @param lastModified the timestamp that came from the Last-Modified header
* @return The value from
* {@link SQLiteDatabase#insert(String, String, android.content.ContentValues)} if a new
* row is inserted (row ID or -1 on error), or the value from
* {@link SQLiteDatabase#update(String, android.content.ContentValues, String, String[])} if an
* existing row
* was updated (number of affected rows)
*/
public long add(@Nullable T in, @Nullable Long lastModified) {
if (in == null) {
return -1;
}
mDb.beginTransaction();
long ret = -1;
try {
if (!exists(in.getKey())) {
if (lastModified != null
&& (in.getLastModified() == null || lastModified > in.getLastModified())) {
in.setLastModified(lastModified);
} else if (in.getLastModified() == null) {
in.setLastModified(0L);
}
ret = mDb.insert(getTableName(), null, in.getParams(mGson));
if (ret != -1) {
insertCallback(in);
}
} else {
ret = update(in, lastModified);
}
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
}
return ret;
}
/**
* Adds a List of items to the database via {@link #add(TbaDatabaseModel, Long)}
* @param inList List of models to be added
* @param lastModified the timestamp that came from the Last-Modified header
* @return the number of rows added
*/
public int add(@Nullable ImmutableList<T> inList, @Nullable Long lastModified){
if (inList == null) {
return 0;
}
int inserted = 0;
try {
mDb.beginTransaction();
for (T in : inList) {
long ret = add(in, lastModified);
if (ret > 0) {
inserted++;
}
}
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
}
return inserted;
}
/**
* Updates an existing row in the database.
* Uses {@link #getKeyColumn()} = {@link TbaDatabaseModel#getKey()} as the WHERE clause
* If the update was successful, call {@link #updateCallback(TbaDatabaseModel)}
* @param in Model to be updated
* @param lastModified the timestamp that came from the Last-Modified header
* @return Value from
* {@link SQLiteDatabase#update(String, android.content.ContentValues, String, String[])},
* or number of rows affected by the query
*/
public int update(@Nullable T in, @Nullable Long lastModified){
int affectedRows;
if (in == null) {
return -1;
}
mDb.beginTransaction();
try {
if (lastModified != null
&& (in.getLastModified() == null || lastModified > in.getLastModified())) {
in.setLastModified(lastModified);
}
if (in.getLastModified() == null) {
in.setLastModified(0L);
}
affectedRows = mDb.update(
getTableName(),
in.getParams(mGson),
getKeyColumn() + " = ? AND ? >= " + getLastModifiedColumn(),
new String[]{in.getKey(), in.getLastModified().toString()});
if (affectedRows > 0) {
updateCallback(in);
}
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
}
return affectedRows;
}
/**
* Fetches a model with all fields from the database by key
* Wrapper for {@link #get(String, String[])}, with null as the second parameter
* @param key Key to match {@link #getKeyColumn()} on
* @return An inflated model found with the provided key
*/
public T get(String key) {
return get(key, null);
}
/**
* Fetch a model with the given fields from the database by key
* @param key Key to match {@link #getKeyColumn()} on
* @param fields String array of database column names to use as a projection
* @return Inflated model from the first row returned in the query
*/
public T get(String key, String[] fields){
Cursor cursor = mDb.query(
getTableName(),
fields,
getKeyColumn() + " = ?",
new String[]{key},
null,
null,
null,
null);
if (cursor != null && cursor.moveToFirst()) {
T model = inflate(cursor);
cursor.close();
return model;
} else {
return null;
}
}
/**
* Query the table. Wrapper for
* {@link SQLiteDatabase#query(String, String[], String, String[], String, String, String, String)}
* @param columns Column projection to fetch
* @param selection WHERE clause (use ? for variables)
* @param selectionArgs Replacement args for the WHERE clause
* @param groupBy GROUP BY clause
* @param having HAVING clause
* @param orderBy ORDER BY clause
* @param limit LIMIT clause
* @return Cursor with the rows returned by the query
*/
public Cursor query(
String[] columns,
String selection,
String[] selectionArgs,
String groupBy,
String having,
String orderBy,
String limit){
return mDb.query(getTableName(), columns, selection, selectionArgs, groupBy, having, orderBy, limit);
}
public List<T> getForQuery(
String[] columns,
String selection,
String[] selectionArgs) {
Cursor cursor = query(columns, selection, selectionArgs, null, null, null, null);
List<T> models = new ArrayList<>(cursor == null ? 0 : cursor.getCount());
if (cursor == null || !cursor.moveToFirst()) {
return models;
}
do {
models.add(inflate(cursor));
} while (cursor.moveToNext());
return models;
}
/**
* Fetches all rows from the table
* Equivalent to SELECT * FROM {@link #getTableName()}
* @return List of all inflated models in the table
*/
public List<T> getAll(){
Cursor cursor = mDb.rawQuery("SELECT * FROM " + getTableName(), new String[]{});
List<T> ret = new ArrayList<>();
if (cursor != null && cursor.moveToFirst()) {
do {
T model = inflate(cursor);
ret.add(model);
} while (cursor.moveToNext());
cursor.close();
}
return ret;
}
/**
* Check if a model with the given key exists
* @param key Key to match {@link #getKeyColumn()} on
* @return true if a row with the given key exists
*/
public boolean exists(String key){
Cursor cursor = mDb.query(
getTableName(),
new String[]{getKeyColumn()},
getKeyColumn() + "=?",
new String[]{key},
null,
null,
null,
null);
boolean result;
if (cursor != null) {
result = cursor.moveToFirst();
cursor.close();
} else {
result = false;
}
return result;
}
/**
* Delete a given model from the database
* Same as running DELETE FROM {@link #getTableName()} WHERE {@link #getKeyColumn()} = key
* If the deletion was successful, call {@link #deleteCallback(TbaDatabaseModel)}
* @param in Model to delete. Uses {@link TbaDatabaseModel#getKey()} for the key
* @return Return value from {@link SQLiteDatabase#delete(String, String, String[])}
* (number of rows affected)
*/
public int delete(T in) {
int ret;
mDb.beginTransaction();
try {
ret = mDb.delete(getTableName(), getKeyColumn() + " = ?", new String[]{in.getKey()});
if (ret > 0) {
deleteCallback(in);
}
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
}
return ret;
}
/**
* Delete from the table with a provided WHERE clause
* DOES NOT CALL {@link #deleteCallback(TbaDatabaseModel)} - can't make the types work :(
* @param whereClause SQL WHERE clause to use in deletion
* @param whereArgs Substitution arguments for the clause
* @return Value from {@link SQLiteDatabase#delete(String, String, String[])}
* (number of rows affected)
*/
public int delete(String whereClause, String[] whereArgs) {
int ret = 0;
mDb.beginTransaction();
try {
ret = mDb.delete(getTableName(), whereClause, whereArgs);
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
}
return ret;
}
/**
* Deletes all rows from this table.
*/
public int deleteAllRows() {
int ret = 0;
mDb.beginTransaction();
try {
ret = mDb.delete(getTableName(), null, null);
if (ret > 0) {
deleteAllCallback();
}
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
}
return ret;
}
/**
* Called after a successful row insert
* Override to let concrete implementations do something with an inserted row
* e.g. add Search Indexes
* @param model Model that was inserted
*/
protected void insertCallback(T model){
// default to no op
}
/**
* Called after a successful row update
* Override to let concrete implementations do something after a row update
* @param model Model that was updated
*/
protected void updateCallback(T model){
// default to no op
}
/**
* Called after a successful row delete
* Override to let concrete implementations do something after a row delete
* @param model Model that wasa deleted
*/
protected void deleteCallback(T model){
// default to no op
}
/**
* Called after deleting all rows in the table
* Override to let extendors do something else they need to
*/
protected void deleteAllCallback() {
// default to no op
}
/**
* @return a String containing the same of the backing SQL table
*/
@VisibleForTesting
public abstract String getTableName();
/**
* @return a String containing the column name of the primary key in the backing table
*/
@VisibleForTesting
public abstract String getKeyColumn();
/**
* @return the column title for the last modified information
*/
public abstract String getLastModifiedColumn();
/**
* Inflates a cursor row from the db to a model class
* Probably uses a method in {@link ModelInflater}
* @param cursor Cursor moved to a row in the database
* @return An inflated model from the input cursor
*/
public abstract T inflate(Cursor cursor);
}