/******************************************************************************* * This file is part of RedReader. * * RedReader is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * RedReader is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with RedReader. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package org.quantumbadger.redreader.io; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import org.quantumbadger.redreader.common.UnexpectedInternalStateException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.Locale; public class RawObjectDB<K, E extends WritableObject<K>> extends SQLiteOpenHelper { private final Class<E> clazz; private final Field[] fields; private final String[] fieldNames; private static final String TABLE_NAME = "objects", FIELD_ID = "RawObjectDB_id", FIELD_TIMESTAMP = "RawObjectDB_timestamp"; private static <E> int getDbVersion(Class<E> clazz) { for(final Field field : clazz.getDeclaredFields()) { if(field.isAnnotationPresent(WritableObject.WritableObjectVersion.class)) { field.setAccessible(true); try { return field.getInt(null); } catch(IllegalAccessException e) { throw new RuntimeException(e); } } } throw new UnexpectedInternalStateException("Writable object has no DB version"); } public RawObjectDB(final Context context, final String dbFilename, final Class<E> clazz) { super(context.getApplicationContext(), dbFilename, null, getDbVersion(clazz)); this.clazz = clazz; final LinkedList<Field> fields = new LinkedList<>(); for(final Field field : clazz.getDeclaredFields()) { if((field.getModifiers() & Modifier.TRANSIENT) == 0 && !field.isAnnotationPresent(WritableObject.WritableObjectKey.class) && !field.isAnnotationPresent(WritableObject.WritableObjectTimestamp.class) && field.isAnnotationPresent(WritableObject.WritableField.class)) { field.setAccessible(true); fields.add(field); } } this.fields = fields.toArray(new Field[fields.size()]); fieldNames = new String[this.fields.length + 2]; for(int i = 0; i < this.fields.length; i++) fieldNames[i] = this.fields[i].getName(); fieldNames[this.fields.length] = FIELD_ID; fieldNames[this.fields.length + 1] = FIELD_TIMESTAMP; } private String getFieldTypeString(Class<?> fieldType) { if(fieldType == Integer.class || fieldType == Long.class || fieldType == Integer.TYPE || fieldType == Long.TYPE) { return " INTEGER"; } else if(fieldType == Boolean.class || fieldType == Boolean.TYPE) { return " INTEGER"; } else { return " TEXT"; } } @Override public void onCreate(final SQLiteDatabase db) { final StringBuilder query = new StringBuilder("CREATE TABLE "); query.append(TABLE_NAME); query.append('('); query.append(FIELD_ID); query.append(" TEXT PRIMARY KEY ON CONFLICT REPLACE,"); query.append(FIELD_TIMESTAMP); query.append(" INTEGER"); for(final Field field : fields) { query.append(','); query.append(field.getName()); query.append(getFieldTypeString(field.getType())); } query.append(')'); Log.i("RawObjectDB query string", query.toString()); db.execSQL(query.toString()); } @Override public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { // TODO detect version from static field/WritableObject method, delete all data, start again } public synchronized Collection<E> getAll() { final SQLiteDatabase db = getReadableDatabase(); try { final Cursor cursor = db.query(TABLE_NAME, fieldNames, null, null, null, null, null); try { final LinkedList<E> result = new LinkedList<>(); while(cursor.moveToNext()) result.add(readFromCursor(cursor)); return result; } catch(InstantiationException e) { throw new RuntimeException(e); } catch(IllegalAccessException e) { throw new RuntimeException(e); } catch(InvocationTargetException e) { throw new RuntimeException(e); } finally { cursor.close(); } } finally { db.close(); } } public synchronized E getById(final K id) { final ArrayList<E> queryResult = getByField(FIELD_ID, id.toString()); if(queryResult.size() != 1) return null; else return queryResult.get(0); } public synchronized ArrayList<E> getByField(final String field, final String value) { final SQLiteDatabase db = getReadableDatabase(); try { final Cursor cursor = db.query(TABLE_NAME, fieldNames, String.format(Locale.US, "%s=?", field), new String[] {value}, null, null, null); try { final ArrayList<E> result = new ArrayList<>(cursor.getCount()); while(cursor.moveToNext()) result.add(readFromCursor(cursor)); return result; } catch(InstantiationException e) { throw new RuntimeException(e); } catch(IllegalAccessException e) { throw new RuntimeException(e); } catch(InvocationTargetException e) { throw new RuntimeException(e); } finally { cursor.close(); } } finally { db.close(); } } private E readFromCursor(final Cursor cursor) throws IllegalAccessException, InstantiationException, InvocationTargetException { final E obj; try { final Constructor<E> constructor = clazz.getConstructor(WritableObject.CreationData.class); final String id = cursor.getString(fields.length); final long timestamp = cursor.getLong(fields.length + 1); obj = constructor.newInstance(new WritableObject.CreationData(id, timestamp)); } catch(NoSuchMethodException e) { throw new RuntimeException(e); } for(int i = 0; i < fields.length; i++) { final Field field = fields[i]; final Class<?> fieldType = field.getType(); if(fieldType == String.class) { field.set(obj, cursor.isNull(i) ? null : cursor.getString(i)); } else if(fieldType == Integer.class) { field.set(obj, cursor.isNull(i) ? null : cursor.getInt(i)); } else if(fieldType == Integer.TYPE) { field.setInt(obj, cursor.getInt(i)); } else if(fieldType == Long.class) { field.set(obj, cursor.isNull(i) ? null : cursor.getLong(i)); } else if(fieldType == Long.TYPE) { field.setLong(obj, cursor.getLong(i)); } else if(fieldType == Boolean.class) { field.set(obj, cursor.isNull(i) ? null : cursor.getInt(i) != 0); } else if(fieldType == Boolean.TYPE) { field.setBoolean(obj, cursor.getInt(i) != 0); } else if(fieldType == WritableHashSet.class) { field.set(obj, cursor.isNull(i) ? null : WritableHashSet.unserializeWithMetadata(cursor.getString(i))); } else { throw new UnexpectedInternalStateException("Invalid readFromCursor field type " + fieldType.getClass().getCanonicalName()); } } return obj; } public synchronized void put(E object) { final SQLiteDatabase db = getWritableDatabase(); try { final ContentValues values = new ContentValues(fields.length + 1); final long result = db.insertOrThrow(TABLE_NAME, null, toContentValues(object, values)); if(result < 0) throw new RuntimeException("Database write failed"); } catch(IllegalAccessException e) { throw new RuntimeException(e); } finally { db.close(); } } public synchronized void putAll(final Collection<E> objects) { final SQLiteDatabase db = getWritableDatabase(); try { final ContentValues values = new ContentValues(fields.length + 1); for(final E object : objects) { final long result = db.insertOrThrow(TABLE_NAME, null, toContentValues(object, values)); if(result < 0) throw new RuntimeException("Bulk database write failed"); } } catch(IllegalAccessException e) { throw new RuntimeException(e); } finally { db.close(); } } private ContentValues toContentValues(final E obj, final ContentValues result) throws IllegalAccessException { result.put(FIELD_ID, obj.getKey().toString()); result.put(FIELD_TIMESTAMP, obj.getTimestamp()); for(int i = 0; i < fields.length; i++) { final Field field = fields[i]; final Class<?> fieldType = field.getType(); if(fieldType == String.class) { result.put(fieldNames[i], (String) field.get(obj)); } else if(fieldType == Integer.class) { result.put(fieldNames[i], (Integer) field.get(obj)); } else if(fieldType == Integer.TYPE) { result.put(fieldNames[i], field.getInt(obj)); } else if(fieldType == Long.class) { result.put(fieldNames[i], (Long) field.get(obj)); } else if (fieldType == Long.TYPE) { result.put(fieldNames[i], field.getLong(obj)); } else if(fieldType == Boolean.class) { final Boolean val = (Boolean) field.get(obj); result.put(fieldNames[i], val == null ? null : (val ? 1 : 0)); } else if(fieldType == Boolean.TYPE) { result.put(fieldNames[i], field.getBoolean(obj) ? 1 : 0); } else if(fieldType == WritableHashSet.class) { result.put(fieldNames[i], ((WritableHashSet)field.get(obj)).serializeWithMetadata()); } else { throw new UnexpectedInternalStateException(); } } return result; } }