package net.bradmont.supergreen.models;
import net.bradmont.supergreen.fields.*;
import net.bradmont.supergreen.fields.constraints.*;
import android.database.sqlite.*;
import android.database.Cursor;
import android.widget.SimpleCursorAdapter;
import android.view.View;
import android.view.MenuItem;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.ArrayList;
import java.text.SimpleDateFormat;
import java.io.PrintWriter;
import java.io.StringWriter;
// TODO: Modify to allow non-integer pirmary keys
// WONTFIX: ListView requires an int pk anyway
// TODO: Add DB version tag to Field
// Write code to auto-generate create & update SQL
public abstract class DBModel {
/** id is kept as a convenience. Table primary keys must be integers. */
protected int id = 0;
LinkedHashMap<String, DBField> fields = new LinkedHashMap<String, DBField>(); // preserves insertion order
HashMap<String, String> fields_by_column_name = new HashMap<String, String>(); // stores columname:fieldname pairs for Fields that have a custom columname
DBField primaryKey;
boolean record_exists = false;
protected static SQLiteOpenHelper dbh = null;
protected SQLiteDatabase dbr = null;
protected static SQLiteDatabase dbw = null;
protected Context context = null;
protected String TABLE_NAME = null;
public DBModel(SQLiteOpenHelper dbh, String table_name){
// create a new DBModel. Won't be written to DB until save() is called.
this.dbh = dbh;
this.TABLE_NAME = table_name;
}
public DBModel(SQLiteOpenHelper dbh, String table_name, int _id){
// loads an existing DBModel from the database by id
this.dbh = dbh;
this.TABLE_NAME = table_name;
if (_id == 0){
throw new RowDoesNotExistException(_id);
}
this.id = _id;
}
/**
* Allows the model to return SQL statements to initialize its table,
* for exampel to populate it with default data. Runs only after initial
* table creation.
*/
public String [] generateInitializeSQL(){
return new String[0];
}
public String generateCreateSQL(){
String sql = String.format("CREATE TABLE %s ( %s );", TABLE_NAME, "%s");
// column definitions:
int i = 0;
String field_defs = "";
for (Map.Entry<String, DBField> entry: fields.entrySet()){
DBField field = entry.getValue();
String oldArgs = field.getExtraArguments();
if (field == primaryKey){
field.setExtraArguments("PRIMARY KEY " + oldArgs);
}
// why doesn't Java have a String.join function?
if (i == 0){
field_defs = field.getSQLDefinition() ;
} else {
field_defs = field_defs + ", " + field.getSQLDefinition() ;
}
if (field == primaryKey){ field.setExtraArguments(oldArgs); }
i++;
}
// table constraints:
i = 0;
String table_constraints = "";
for (Map.Entry<String, DBField> entry: fields.entrySet()){
DBField field = entry.getValue();
String constraint = field.getSQLTableConstraint();
if (constraint != ""){
if (i == 0){
table_constraints = constraint;
} else {
table_constraints = table_constraints + ", " + constraint;
}
}
i++;
}
sql = String.format(sql, field_defs + " " + table_constraints);
return sql;
}
public String [] generateUpdateSQL(int oldversion){
// TODO
return new String[0];
}
public void setContext(Context context){
this.context = context;
}
public Context getContext() { return context;}
protected void addField(DBField field){
field.setModel(this);
fields.put(field.getName(), field);
if (field.getName() != field.getColumnName()){
fields_by_column_name.put(field.getColumnName(), field.getName());
}
}
public void setFieldColumnName(String fieldName, String columnName){
// record that a field doesn't use the same column name as its field name
fields_by_column_name.put(columnName, fieldName);
}
protected void setPrimaryKey(DBField field){
primaryKey = field;
}
public DBField getPrimaryKey(){
return primaryKey;
}
/**
* Get a DBField belonging to this DBModel
*/
public DBField getField(String name){
return fields.get(name);
}
/**
* Directly retrieve the int value of a field
*/
public int getInt(String fieldName){
return getField(fieldName).getInt();
}
/**
* Directly retrieve the String value of a field
*/
public String getString(String fieldName){
return getField(fieldName).getString();
}
/**
* Directly retrieve the float value of a field
*/
public float getFloat(String fieldName){
return getField(fieldName).getFloat();
}
/**
* Directly retrieve the boolean value of a field
*/
public boolean getBoolean(String fieldName){
return getField(fieldName).getBoolean();
}
/**
* Directly retrieve the Date value of a field
*/
public Calendar getCalendar(String fieldName){
return getField(fieldName).getCalendar();
}
/**
* Directly retrieve a DBModel of a related field
*/
public DBModel getRelated(String fieldName){
return getField(fieldName).getRelated();
}
/**
* Retrive an array of DBModels for a One-to-Many relationship.
* relatedModel is an instance of the DBModel that references this DBModel
* fieldName is the ForeignKey of relatedModel that refers to this DBModel
*/
public ModelList getRelatedList(DBModel relatedModel, String fieldName){
return relatedModel.filter(fieldName, id);
}
/**
* Return a list of all instances of this model.
*/
public ModelList getAll(){
return new ModelList(this, null, null);
}
/**
* Return a list of this model, filtering where fieldName = value
*/
public ModelList filter(String fieldName, int value){
return new ModelList(this, fieldName, value);
}
/**
* Return a list of this model, filtering where fieldName = value
*/
public ModelList filter(String fieldName, float value){
return new ModelList(this, fieldName, value);
}
/**
* Return a list of this model, filtering where fieldName = value
*/
public ModelList filter(String fieldName, String value){
return new ModelList(this, fieldName, value);
}
/**
* Return a list of this model, filtering where fieldName = value
*/
public ModelList filter(String fieldName, boolean value){
return new ModelList(this, fieldName, value);
}
/**
* Get an instance by a unique value other than primary key. No
* sanity checking is done, so verify that row is unique.
*/
public DBModel getByField(String field, int value){
return getByField(field, Integer.toString(value));
}
/**
* Get an instance by a unique value other than primary key. No
* sanity checking is done, so verify that row is unique.
*/
public DBModel getByField(String field, String value){
String [] args = new String[1];
String [] columns = new String[1];
args[0] = value;
columns[0] = getPrimaryKey().getColumnName();
Cursor c = getDbr().query(getTableName(), columns, String.format("%s = ?", field), args, null, null, null);
if (c.getCount() == 0) { return null;}
c.moveToFirst();
DBModel result = newInstance(c.getInt(0));
c.close();
return result;
}
// Directly assign values to fields:
/**
* Set the value of a field to an int, for supported fields
*/
public void setValue(String fieldName, int value){
getField(fieldName).setValue(value);
}
/**
* Set the value of a field to a String, for supported fields
*/
public void setValue(String fieldName, String value){
getField(fieldName).setValue(value);
}
/**
* Set the value of a field to a Float, for supported fields
*/
public void setValue(String fieldName, float value){
getField(fieldName).setValue(value);
}
/**
* Set the value of a field to a boolean, for supported fields
*/
public void setValue(String fieldName, boolean value){
getField(fieldName).setValue(value);
}
/**
* Set the value of a field to a Date, for supported fields
*/
public void setValue(String fieldName, Calendar value){
getField(fieldName).setValue(value);
}
/**
* Set the value of a ForeignKey to a related DBModel
*/
public void setValue(String fieldName, DBModel value){
getField(fieldName).setValue(value);
}
public Set<String> getKeys(){
return fields.keySet();
}
/**
* returns the name of the table this model is using
*/
public String getTableName(){
return TABLE_NAME;
}
protected void init(){
if (id != 0){ // only load if it's apropriate
String sql = String.format("select * from %s where %s=%d",
TABLE_NAME, primaryKey.getColumnName(), id);
setupDbr();
Cursor cur = getDbr().rawQuery(sql, null);
if (cur.getCount() == 0){
throw new RowDoesNotExistException(id);
} else if (cur.getCount() > 1){
throw new RowDoesNotExistException("Record is not unique.", id);
}
cur.moveToFirst();
loadRecord(cur);
cur.close();
record_exists = true;
}
}
/**
* Attempt to save the model. If any fields don't meet their constraints,
* this will throw ConstraintError. Returns the new object id.
*/
public int save() throws ConstraintError{
// if the record exists, update it. Otherwise, save it.
validate();
if (record_exists){
update();
} else {
create_record();
}
return id;
}
/**
* Attempts to save the model, and discards any exceptions. Only do this
* if you know the field values will match any constraints.
*/
public int dirtySave(){
try {
return save();
} catch (Exception e){
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
Log.i("net.bradmont.supergreen",sw.toString());
return 0;
}
}
public int getID(){
return id;
}
public void update(){
setupDbw();
dbw.update(TABLE_NAME, buildContentValues(true), String.format("%s=%d", primaryKey.getColumnName(), id), null);
//Log.i("net.bradmont.supergreen", String.format("update: %s %s=%d", TABLE_NAME, primaryKey.getColumnName(), id));
}
public void delete(){
if (record_exists == true){
setupDbw();
dbw.delete(TABLE_NAME, String.format("%s=%d", primaryKey.getColumnName(), id), null);
record_exists = false;
}
}
private int create_record(){
setupDbw();
if (primaryKey.getInt() != 0){
id = (int) dbw.insert(TABLE_NAME, null, buildContentValues(true)) ;
} else {
id = (int) dbw.insert(TABLE_NAME, null, buildContentValues(false)) ;
}
((IntField)primaryKey).setValue(id);
record_exists = true;
return id;
}
/**
DEPRECATED: move to view handler
*/
public String getListSQL(){
String SQL = "SELECT * from " + TABLE_NAME ;
return SQL;
}
void loadRecord(Cursor cur){
for (Map.Entry<String, DBField> entry: fields.entrySet()){
DBField field = entry.getValue();
int colIndex = cur.getColumnIndex(field.getColumnName());
if (field instanceof IntField) {
((IntField) field).setValue(cur.getInt(colIndex));
} else if (field instanceof BooleanField) {
((BooleanField) field).setValue(cur.getInt(colIndex));
} else if (field instanceof DateField) {
((DateField) field).setValue(cur.getString(colIndex));
} else if (field instanceof StringField) {
((StringField) field).setValue(cur.getString(colIndex));
} else if (field instanceof FloatField) {
((FloatField) field).setValue(cur.getFloat(colIndex));
}
}
}
/**
* Instantiate a copy of this class.
*/
public abstract DBModel newInstance();
public abstract DBModel newInstance(int id);
/**
* Populate fields by directly providing values. Requires a cursor resulting from a SELECT * FROM TABLE. The cursor must have been move'd to the relevant record.
* Added to increase performance when populating multiple DBModels at once, allowing a single query rather than requrying for every DBModel.
*/
public DBModel loadFromRow(Cursor cur){
String [] columns = cur.getColumnNames();
for (int i = 0; i < cur.getColumnCount(); i++){
DBField field = fields.get(columns[i]);
if (field == null){
field = fields.get(
fields_by_column_name.get(columns[i]));
}
// populate field by data type
if (field instanceof IntField){
field.setValue(cur.getInt(i));
if (field == primaryKey){
id = cur.getInt(i);
}
} else if (field instanceof FloatField){
field.setValue(cur.getFloat(i));
} else if (field instanceof BooleanField){
field.setValue(cur.getInt(i));
} else { // field instanceof StringField
field.setValue(cur.getString(i));
}
}
record_exists = true;
return this;
}
public boolean validate() throws ConstraintError{
// TODO // Adjust to return all errors at once rather than just the first
for (Map.Entry<String, DBField> entry: fields.entrySet()){
DBField field = entry.getValue();
field.validate();
}
return true;
}
protected ContentValues buildContentValues(){
return buildContentValues(false);
}
protected ContentValues buildContentValues(boolean includePrimaryKey){
ContentValues cv = new ContentValues();
for (Map.Entry<String, DBField> entry: fields.entrySet()){
DBField field = entry.getValue();
if (field != primaryKey || includePrimaryKey){ // reference compare
if (field instanceof IntField) {
cv.put(field.getColumnName(), field.getInt());
} else if (field instanceof BooleanField) {
cv.put(field.getColumnName(), field.getBoolean());
} else if (field instanceof DateField) {
cv.put(field.getColumnName(), field.getString());
} else if (field instanceof StringField) {
cv.put(field.getColumnName(), field.getString());
} else if (field instanceof FloatField) {
cv.put(field.getColumnName(), field.getFloat());
}
}
}
return cv;
}
protected SQLiteDatabase getDbw(){
setupDbw();
return dbw;
}
protected static void setupDbw(){
if (dbw == null){
dbw = dbh.getWritableDatabase();
}
}
protected SQLiteDatabase getDbr(){
setupDbr();
return dbr;
}
private void setupDbr(){
if (dbr == null){
dbr = dbh.getReadableDatabase();
}
}
public SQLiteOpenHelper getDbh(){
return dbh;
}
public class RowDoesNotExistException extends RuntimeException{
String error = "No such record. ";
public RowDoesNotExistException(){
}
public RowDoesNotExistException(int row){
error=error + String.format(" pk = %d", row);
}
public RowDoesNotExistException(String error, int row){
this.error=error + String.format(" pk = %d", row);
}
public String getError(){
return error;
}
}
public boolean recordExists(){
return record_exists;
}
public static void beginTransaction(){
setupDbw();
dbw.beginTransaction();
}
public static void endTransaction(){
dbw.setTransactionSuccessful();
dbw.endTransaction();
}
public void close(){
dbh = null;
dbw = null;
dbr=null;
}
}