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; } }