blob: 7bf7e431c376d55a2d5fa9ffef0765f4717064cf [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gallery3d.common;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.ArrayList;
public final class EntrySchema {
@SuppressWarnings("unused")
private static final String TAG = "EntrySchema";
public static final int TYPE_STRING = 0;
public static final int TYPE_BOOLEAN = 1;
public static final int TYPE_SHORT = 2;
public static final int TYPE_INT = 3;
public static final int TYPE_LONG = 4;
public static final int TYPE_FLOAT = 5;
public static final int TYPE_DOUBLE = 6;
public static final int TYPE_BLOB = 7;
private static final String SQLITE_TYPES[] = {
"TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" };
private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext";
private final String mTableName;
private final ColumnInfo[] mColumnInfo;
private final String[] mProjection;
private final boolean mHasFullTextIndex;
public EntrySchema(Class<? extends Entry> clazz) {
// Get table and column metadata from reflection.
ColumnInfo[] columns = parseColumnInfo(clazz);
mTableName = parseTableName(clazz);
mColumnInfo = columns;
// Cache the list of projection columns and check for full-text columns.
String[] projection = {};
boolean hasFullTextIndex = false;
if (columns != null) {
projection = new String[columns.length];
for (int i = 0; i != columns.length; ++i) {
ColumnInfo column = columns[i];
projection[i] = column.name;
if (column.fullText) {
hasFullTextIndex = true;
}
}
}
mProjection = projection;
mHasFullTextIndex = hasFullTextIndex;
}
public String getTableName() {
return mTableName;
}
public ColumnInfo[] getColumnInfo() {
return mColumnInfo;
}
public String[] getProjection() {
return mProjection;
}
public int getColumnIndex(String columnName) {
for (ColumnInfo column : mColumnInfo) {
if (column.name.equals(columnName)) {
return column.projectionIndex;
}
}
return -1;
}
public ColumnInfo getColumn(String columnName) {
int index = getColumnIndex(columnName);
return (index < 0) ? null : mColumnInfo[index];
}
private void logExecSql(SQLiteDatabase db, String sql) {
db.execSQL(sql);
}
public <T extends Entry> T cursorToObject(Cursor cursor, T object) {
try {
for (ColumnInfo column : mColumnInfo) {
int columnIndex = column.projectionIndex;
Field field = column.field;
switch (column.type) {
case TYPE_STRING:
field.set(object, cursor.isNull(columnIndex)
? null
: cursor.getString(columnIndex));
break;
case TYPE_BOOLEAN:
field.setBoolean(object, cursor.getShort(columnIndex) == 1);
break;
case TYPE_SHORT:
field.setShort(object, cursor.getShort(columnIndex));
break;
case TYPE_INT:
field.setInt(object, cursor.getInt(columnIndex));
break;
case TYPE_LONG:
field.setLong(object, cursor.getLong(columnIndex));
break;
case TYPE_FLOAT:
field.setFloat(object, cursor.getFloat(columnIndex));
break;
case TYPE_DOUBLE:
field.setDouble(object, cursor.getDouble(columnIndex));
break;
case TYPE_BLOB:
field.set(object, cursor.isNull(columnIndex)
? null
: cursor.getBlob(columnIndex));
break;
}
}
return object;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private void setIfNotNull(Field field, Object object, Object value)
throws IllegalAccessException {
if (value != null) field.set(object, value);
}
/**
* Converts the ContentValues to the object. The ContentValues may not
* contain values for all the fields in the object.
*/
public <T extends Entry> T valuesToObject(ContentValues values, T object) {
try {
for (ColumnInfo column : mColumnInfo) {
String columnName = column.name;
Field field = column.field;
switch (column.type) {
case TYPE_STRING:
setIfNotNull(field, object, values.getAsString(columnName));
break;
case TYPE_BOOLEAN:
setIfNotNull(field, object, values.getAsBoolean(columnName));
break;
case TYPE_SHORT:
setIfNotNull(field, object, values.getAsShort(columnName));
break;
case TYPE_INT:
setIfNotNull(field, object, values.getAsInteger(columnName));
break;
case TYPE_LONG:
setIfNotNull(field, object, values.getAsLong(columnName));
break;
case TYPE_FLOAT:
setIfNotNull(field, object, values.getAsFloat(columnName));
break;
case TYPE_DOUBLE:
setIfNotNull(field, object, values.getAsDouble(columnName));
break;
case TYPE_BLOB:
setIfNotNull(field, object, values.getAsByteArray(columnName));
break;
}
}
return object;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public void objectToValues(Entry object, ContentValues values) {
try {
for (ColumnInfo column : mColumnInfo) {
String columnName = column.name;
Field field = column.field;
switch (column.type) {
case TYPE_STRING:
values.put(columnName, (String) field.get(object));
break;
case TYPE_BOOLEAN:
values.put(columnName, field.getBoolean(object));
break;
case TYPE_SHORT:
values.put(columnName, field.getShort(object));
break;
case TYPE_INT:
values.put(columnName, field.getInt(object));
break;
case TYPE_LONG:
values.put(columnName, field.getLong(object));
break;
case TYPE_FLOAT:
values.put(columnName, field.getFloat(object));
break;
case TYPE_DOUBLE:
values.put(columnName, field.getDouble(object));
break;
case TYPE_BLOB:
values.put(columnName, (byte[]) field.get(object));
break;
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public String toDebugString(Entry entry) {
try {
StringBuilder sb = new StringBuilder();
sb.append("ID=").append(entry.id);
for (ColumnInfo column : mColumnInfo) {
String columnName = column.name;
Field field = column.field;
Object value = field.get(entry);
sb.append(" ").append(columnName).append("=")
.append((value == null) ? "null" : value.toString());
}
return sb.toString();
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public String toDebugString(Entry entry, String... columnNames) {
try {
StringBuilder sb = new StringBuilder();
sb.append("ID=").append(entry.id);
for (String columnName : columnNames) {
ColumnInfo column = getColumn(columnName);
Field field = column.field;
Object value = field.get(entry);
sb.append(" ").append(columnName).append("=")
.append((value == null) ? "null" : value.toString());
}
return sb.toString();
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Cursor queryAll(SQLiteDatabase db) {
return db.query(mTableName, mProjection, null, null, null, null, null);
}
public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) {
Cursor cursor = db.query(mTableName, mProjection, "_id=?",
new String[] {Long.toString(id)}, null, null, null);
boolean success = false;
if (cursor.moveToFirst()) {
cursorToObject(cursor, entry);
success = true;
}
cursor.close();
return success;
}
public long insertOrReplace(SQLiteDatabase db, Entry entry) {
ContentValues values = new ContentValues();
objectToValues(entry, values);
if (entry.id == 0) {
values.remove("_id");
}
long id = db.replace(mTableName, "_id", values);
entry.id = id;
return id;
}
public boolean deleteWithId(SQLiteDatabase db, long id) {
return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1;
}
public void createTables(SQLiteDatabase db) {
// Wrapped class must have a @Table.Definition.
String tableName = mTableName;
Utils.assertTrue(tableName != null);
// Add the CREATE TABLE statement for the main table.
StringBuilder sql = new StringBuilder("CREATE TABLE ");
sql.append(tableName);
sql.append(" (_id INTEGER PRIMARY KEY AUTOINCREMENT");
StringBuilder unique = new StringBuilder();
for (ColumnInfo column : mColumnInfo) {
if (!column.isId()) {
sql.append(',');
sql.append(column.name);
sql.append(' ');
sql.append(SQLITE_TYPES[column.type]);
if (!TextUtils.isEmpty(column.defaultValue)) {
sql.append(" DEFAULT ");
sql.append(column.defaultValue);
}
if (column.unique) {
if (unique.length() == 0) {
unique.append(column.name);
} else {
unique.append(',').append(column.name);
}
}
}
}
if (unique.length() > 0) {
sql.append(",UNIQUE(").append(unique).append(')');
}
sql.append(");");
logExecSql(db, sql.toString());
sql.setLength(0);
// Create indexes for all indexed columns.
for (ColumnInfo column : mColumnInfo) {
// Create an index on the indexed columns.
if (column.indexed) {
sql.append("CREATE INDEX ");
sql.append(tableName);
sql.append("_index_");
sql.append(column.name);
sql.append(" ON ");
sql.append(tableName);
sql.append(" (");
sql.append(column.name);
sql.append(");");
logExecSql(db, sql.toString());
sql.setLength(0);
}
}
if (mHasFullTextIndex) {
// Add an FTS virtual table if using full-text search.
String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX;
sql.append("CREATE VIRTUAL TABLE ");
sql.append(ftsTableName);
sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY");
for (ColumnInfo column : mColumnInfo) {
if (column.fullText) {
// Add the column to the FTS table.
String columnName = column.name;
sql.append(',');
sql.append(columnName);
sql.append(" TEXT");
}
}
sql.append(");");
logExecSql(db, sql.toString());
sql.setLength(0);
// Build an insert statement that will automatically keep the FTS
// table in sync.
StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO ");
insertSql.append(ftsTableName);
insertSql.append(" (_id");
for (ColumnInfo column : mColumnInfo) {
if (column.fullText) {
insertSql.append(',');
insertSql.append(column.name);
}
}
insertSql.append(") VALUES (new._id");
for (ColumnInfo column : mColumnInfo) {
if (column.fullText) {
insertSql.append(",new.");
insertSql.append(column.name);
}
}
insertSql.append(");");
String insertSqlString = insertSql.toString();
// Add an insert trigger.
sql.append("CREATE TRIGGER ");
sql.append(tableName);
sql.append("_insert_trigger AFTER INSERT ON ");
sql.append(tableName);
sql.append(" FOR EACH ROW BEGIN ");
sql.append(insertSqlString);
sql.append("END;");
logExecSql(db, sql.toString());
sql.setLength(0);
// Add an update trigger.
sql.append("CREATE TRIGGER ");
sql.append(tableName);
sql.append("_update_trigger AFTER UPDATE ON ");
sql.append(tableName);
sql.append(" FOR EACH ROW BEGIN ");
sql.append(insertSqlString);
sql.append("END;");
logExecSql(db, sql.toString());
sql.setLength(0);
// Add a delete trigger.
sql.append("CREATE TRIGGER ");
sql.append(tableName);
sql.append("_delete_trigger AFTER DELETE ON ");
sql.append(tableName);
sql.append(" FOR EACH ROW BEGIN DELETE FROM ");
sql.append(ftsTableName);
sql.append(" WHERE _id = old._id; END;");
logExecSql(db, sql.toString());
sql.setLength(0);
}
}
public void dropTables(SQLiteDatabase db) {
String tableName = mTableName;
StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS ");
sql.append(tableName);
sql.append(';');
logExecSql(db, sql.toString());
sql.setLength(0);
if (mHasFullTextIndex) {
sql.append("DROP TABLE IF EXISTS ");
sql.append(tableName);
sql.append(FULL_TEXT_INDEX_SUFFIX);
sql.append(';');
logExecSql(db, sql.toString());
}
}
public void deleteAll(SQLiteDatabase db) {
StringBuilder sql = new StringBuilder("DELETE FROM ");
sql.append(mTableName);
sql.append(";");
logExecSql(db, sql.toString());
}
private String parseTableName(Class<? extends Object> clazz) {
// Check for a table annotation.
Entry.Table table = clazz.getAnnotation(Entry.Table.class);
if (table == null) {
return null;
}
// Return the table name.
return table.value();
}
private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) {
ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>();
while (clazz != null) {
parseColumnInfo(clazz, columns);
clazz = clazz.getSuperclass();
}
// Return a list.
ColumnInfo[] columnList = new ColumnInfo[columns.size()];
columns.toArray(columnList);
return columnList;
}
private void parseColumnInfo(Class<? extends Object> clazz, ArrayList<ColumnInfo> columns) {
// Gather metadata from each annotated field.
Field[] fields = clazz.getDeclaredFields(); // including non-public fields
for (int i = 0; i != fields.length; ++i) {
// Get column metadata from the annotation.
Field field = fields[i];
Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class);
if (info == null) continue;
// Determine the field type.
int type;
Class<?> fieldType = field.getType();
if (fieldType == String.class) {
type = TYPE_STRING;
} else if (fieldType == boolean.class) {
type = TYPE_BOOLEAN;
} else if (fieldType == short.class) {
type = TYPE_SHORT;
} else if (fieldType == int.class) {
type = TYPE_INT;
} else if (fieldType == long.class) {
type = TYPE_LONG;
} else if (fieldType == float.class) {
type = TYPE_FLOAT;
} else if (fieldType == double.class) {
type = TYPE_DOUBLE;
} else if (fieldType == byte[].class) {
type = TYPE_BLOB;
} else {
throw new IllegalArgumentException(
"Unsupported field type for column: " + fieldType.getName());
}
// Add the column to the array.
int index = columns.size();
columns.add(new ColumnInfo(info.value(), type, info.indexed(), info.unique(),
info.fullText(), info.defaultValue(), field, index));
}
}
public static final class ColumnInfo {
private static final String ID_KEY = "_id";
public final String name;
public final int type;
public final boolean indexed;
public final boolean unique;
public final boolean fullText;
public final String defaultValue;
public final Field field;
public final int projectionIndex;
public ColumnInfo(String name, int type, boolean indexed, boolean unique,
boolean fullText, String defaultValue, Field field, int projectionIndex) {
this.name = name.toLowerCase();
this.type = type;
this.indexed = indexed;
this.unique = unique;
this.fullText = fullText;
this.defaultValue = defaultValue;
this.field = field;
this.projectionIndex = projectionIndex;
field.setAccessible(true); // in order to set non-public fields
}
public boolean isId() {
return ID_KEY.equals(name);
}
}
}