package org.pyneo.thinstore;
// see http://developer.android.com/training/basics/data-storage/databases.html
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteProgram;
import android.database.sqlite.SQLiteStatement;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.lang.reflect.Field;
import java.util.Locale;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.TimeZone;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* a thin, simple orm made for android. it utilizes the highest abstraction of
* sqlite avaiable in android. no sql is generated by string operations
* (beside create/drop table). reflection is used to allow compile time checks
* of flows (beside the column name in the where and order clauses) and no
* additional columns are added to the pojos (beside the id column that is
* filled by android on insert). usage: create a pojo, add some members, derive
* from StoreObject and you're done. static and volatile members wont be
* persisted. on any db related operation you have to deliver a database object
* which stays in your control. you have to provide the SQLiteOpenHelper and
* use the create() methods to create the tables in its onCreate() method. you
* can use the query class which allows you to write nice stuff like query(db,
* Pojo.class).where("name").equal("Pojo
* 0").and("description").like("P%").order_by("timestamp").fetchAll()
*/
public class StoreObject {
private static final SimpleDateFormat ISO_DATE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
private static final String TAG = "org.pyneo";
/**
* supported java datatypes. most native types plus string and date.
*/
private static final Set<Class> SUPPORTED_TYPES = new HashSet<Class>(Arrays.asList(new Class[] {
String.class,
Date.class,
Boolean.TYPE,
Double.TYPE,
Float.TYPE,
Long.TYPE,
Integer.TYPE,
Short.TYPE,
}));
private static final Map<Class, Field[]> FIELDS_CACHE = new WeakHashMap();
private static final String ID_WHERE = "id = ?";
static {
ISO_DATE.setTimeZone(TimeZone.getTimeZone("UTC"));
}
/**
* primary key for all StoreObject
*/
protected long id = -1l;
public long getId() {
return id;
}
/**
* method to determine a list of fields that will be mapped to a sql table
* column. the result will be cached in a WeakHashMap.
*/
static Field[] storedFields(Class clazz) {
Field[] fields = FIELDS_CACHE.get(clazz);
if (fields == null) {
Class zuper = clazz.getSuperclass();
List<Field> _fields;
if (zuper != Object.class) {
_fields = new ArrayList<>(Arrays.asList(storedFields(zuper)[0]));
}
else {
_fields = new ArrayList<>();
}
for (Field field: clazz.getDeclaredFields()) {
//if ((field.getModifiers() & Modifier.PROTECTED) == 0) continue;
if ((field.getModifiers() & (0
|Modifier.STATIC
|Modifier.VOLATILE
)) != 0)
continue;
if ((field.getModifiers() & Modifier.PUBLIC) == 0)
field.setAccessible(true);
if (field.getName().startsWith("this$")) // outer class reference :)
continue;
if (!SUPPORTED_TYPES.contains(field.getType()))
continue;
_fields.add(field);
}
fields = _fields.toArray(new Field[0]);
FIELDS_CACHE.put(clazz, fields);
}
return fields;
}
/**
*
*/
static String[] getProjection(Class clazz) {
int i = 0;
Field[] fields = storedFields(clazz);
String[] ret = new String[fields.length];
for (Field field: fields) {
ret[i++] = field.getName();
}
//Log.d(TAG, "projection=" + Arrays.toString(ret));
return ret;
}
/**
*
*/
ContentValues toContentValues(ContentValues contentValues) throws Exception {
for (Field field: storedFields(this.getClass())) {
Class clazz = field.getType();
if ("id".equals(field.getName())) {
; // ignore
}
else if (clazz == String.class) {
String v = (String)field.get(this);
if (v != null)
contentValues.put(field.getName(), v);
}
else if (clazz == Boolean.TYPE) {
contentValues.put(field.getName(), field.getBoolean(this)?"true":"false");
}
else if (clazz == Double.TYPE) {
contentValues.put(field.getName(), field.getDouble(this));
}
else if (clazz == Float.TYPE) {
contentValues.put(field.getName(), field.getFloat(this));
}
else if (clazz == Long.TYPE) {
contentValues.put(field.getName() ,field.getLong(this));
}
else if (clazz == Integer.TYPE) {
contentValues.put(field.getName(), field.getInt(this));
}
else if (clazz == Short.TYPE) {
contentValues.put(field.getName(), field.getShort(this));
}
else if (clazz == Date.class) {
Date v = (Date)field.get(this);
if (v != null)
contentValues.put(field.getName(), ISO_DATE.format(v));
}
else {
throw new Exception("unsupported clazz=" + clazz.getName());
}
}
return contentValues;
}
/**
*
*/
StoreObject fromCursor(Cursor cursor) throws Exception {
Field[] fields = storedFields(this.getClass());
for (int i=0;i<fields.length;i++) {
Class clazz = fields[i].getType();
if (cursor.isNull(i))
fields[i].set(this, null);
else if (clazz == String.class)
fields[i].set(this, cursor.getString(i));
else if (clazz == Boolean.TYPE)
fields[i].setBoolean(this, "true".equalsIgnoreCase(cursor.getString(i)));
else if (clazz == Double.TYPE)
fields[i].setDouble(this, cursor.getDouble(i));
else if (clazz == Float.TYPE)
fields[i].setFloat(this, cursor.getFloat(i));
else if (clazz == Long.TYPE)
fields[i].setLong(this, cursor.getLong(i));
else if (clazz == Integer.TYPE)
fields[i].setInt(this, cursor.getInt(i));
else if (clazz == Short.TYPE)
fields[i].setShort(this, cursor.getShort(i));
else if (clazz == Date.class)
fields[i].set(this, ISO_DATE.parse(cursor.getString(i)));
else
throw new Exception("unsupported clazz=" + clazz.getName());
}
return this;
}
/**
*
*/
protected StoreObject add(SQLiteDatabase db, List list, StoreObject item) throws Exception {
list.add(item);
return item.insert(db);
}
/**
* create the table in the database.
*/
public static String create(SQLiteDatabase db, Class clazz) {
String ret = "CREATE TABLE ";
ret += clazz.getSimpleName();
ret += " (";
for (Field field: storedFields(clazz)) {
if ("id".equals(field.getName())) {
ret += "id INTEGER PRIMARY KEY AUTOINCREMENT";
}
else {
ret += ", ";
ret += field.getName();
}
}
ret += ")";
if (db != null) {
db.execSQL(ret);
}
return ret;
}
/**
* alter the table in the database to fit class. warning: total data loss!
* TODO not implemented properly yet: drops&creates for now
*/
public static String alter(SQLiteDatabase db, Class clazz) {
try {
drop(db, clazz);
}
catch (android.database.sqlite.SQLiteException e) {
}
create(db, clazz);
return null;
}
/**
* insert this record into the database.
*/
public StoreObject insert(SQLiteDatabase db) throws Exception {
if (id >= 0) {
throw new Exception("id=" + id);
}
id = db.insert(this.getClass().getSimpleName(), "null", toContentValues(new ContentValues()));
// Log.d(TAG, "new item id=" + id);
return this;
}
/**
* selects records from the database. mainly intern usage, use query object
* to control where and order filters.
*/
public static List<StoreObject> select(SQLiteDatabase db, Class clazz, String where, String[] values, String order) throws Exception {
//Log.d(TAG, "StoreObject.select select=" + Arrays.toString(getProjection(clazz)) + ", from=" + clazz.getSimpleName() + ", where=" + where + ", values=" + Arrays.toString(values));
List<StoreObject> list = new ArrayList();
try (final Cursor cursor = db.query(clazz.getSimpleName(),
getProjection(clazz),
where, // where
values, // values
null, // group
null, // having
order)) { // order
while (cursor.moveToNext()) {
list.add(((StoreObject)clazz.getConstructor().newInstance()).fromCursor(cursor));
}
}
// Log.d(TAG, "StoreObject.select list.size=" + list.size());
return list;
}
/**
* select records from the database. all records for one class will be derived.
*/
public static List<StoreObject> select(SQLiteDatabase db, Class clazz) throws Exception {
return select(db, clazz, null, null, "id");
}
/**
* start a query. creates a query objects where filter can be applied and
* finally a fetch can be done.
*/
public static Query query(SQLiteDatabase db, Class clazz) throws Exception {
return new Query(db, clazz);
}
/**
* query object that holds the data to finally issue the query.
*/
public static class Query {
SQLiteDatabase db;
Class clazz;
String where;
List<String> values = new ArrayList();
String order;
Query(SQLiteDatabase db, Class clazz) {
this.db = db;
this.clazz = clazz;
}
public Query where(String column) {
// TODO check column
where = column;
return this;
}
public Query equal(Object o) {
where += " = ";
if (o instanceof String) {
where += '?';
values.add(o.toString());
}
else if (o instanceof Boolean) {
where += (Boolean)o?"'true'":"'false'";
}
else if (o instanceof Date) {
where += '?';
values.add(ISO_DATE.format((Date)o));
}
else {
where += o;
}
return this;
}
public Query like(Object o) {
where += " LIKE ?";
values.add(o.toString());
return this;
}
public Query and(String column) {
// TODO check column
where += " AND " + column;
return this;
}
public Query order_by(String column) {
// TODO check column
if (order == null) {
order = column;
}
else {
order += ", " + column;
}
return this;
}
public StoreObject fetchOne() throws Exception {
StoreObject obj = null;
try (final Cursor cursor = db.query(clazz.getSimpleName(),
getProjection(clazz),
where, // where
values.toArray(new String[0]), // values
null, // group
null, // having
order)) { // order
if (!cursor.moveToNext()) {
throw new Exception("none found");
}
obj = ((StoreObject)clazz.getConstructor().newInstance()).fromCursor(cursor);
if (cursor.moveToNext()) {
throw new Exception("not unique");
}
}
return obj;
}
public List<StoreObject> fetchAll() throws Exception {
return select(db, clazz, where, values.toArray(new String[0]), order);
}
public int delete() throws Exception {
return db.delete(clazz.getSimpleName(), where, values.toArray(new String[0]));
}
public long count() {
return -1l;
}
}
/**
* update this record in the database.
*/
public StoreObject update(SQLiteDatabase db) throws Exception {
if (id < 0) {
throw new Exception("id=" + id);
}
if (db.update(this.getClass().getSimpleName(), toContentValues(new ContentValues()), ID_WHERE, new String[]{String.valueOf(id)}) != 1) {
throw new Exception("did not update 1");
}
return this;
}
/**
* delete this record from the database.
*/
public void delete(SQLiteDatabase db) throws Exception {
if (id < 0) {
throw new Exception("id=" + id);
}
if (db.delete(this.getClass().getSimpleName(), ID_WHERE, new String[]{String.valueOf(id)}) != 1) {
throw new Exception("did not delete 1");
}
}
/**
* drop the table from the database.
*/
public static String drop(SQLiteDatabase db, Class clazz) {
String ret = "DROP TABLE ";
ret += clazz.getSimpleName();
if (db != null) {
db.execSQL(ret);
}
return ret;
}
}