1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.cooliris.picasa; 18 19 import java.lang.reflect.AnnotatedElement; 20 import java.lang.reflect.Field; 21 import java.util.ArrayList; 22 23 import android.content.ContentValues; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.util.Log; 27 28 public final class EntrySchema { 29 public static final int TYPE_STRING = 0; 30 public static final int TYPE_BOOLEAN = 1; 31 public static final int TYPE_SHORT = 2; 32 public static final int TYPE_INT = 3; 33 public static final int TYPE_LONG = 4; 34 public static final int TYPE_FLOAT = 5; 35 public static final int TYPE_DOUBLE = 6; 36 public static final int TYPE_BLOB = 7; 37 public static final String SQLITE_TYPES[] = { "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" }; 38 39 private static final String TAG = "SchemaInfo"; 40 private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext"; 41 42 private final String mTableName; 43 private final ColumnInfo[] mColumnInfo; 44 private final String[] mProjection; 45 private final boolean mHasFullTextIndex; 46 EntrySchema(Class<? extends Entry> clazz)47 public EntrySchema(Class<? extends Entry> clazz) { 48 // Get table and column metadata from reflection. 49 ColumnInfo[] columns = parseColumnInfo(clazz); 50 mTableName = parseTableName(clazz); 51 mColumnInfo = columns; 52 53 // Cache the list of projection columns and check for full-text columns. 54 String[] projection = {}; 55 boolean hasFullTextIndex = false; 56 if (columns != null) { 57 projection = new String[columns.length]; 58 for (int i = 0; i != columns.length; ++i) { 59 ColumnInfo column = columns[i]; 60 projection[i] = column.name; 61 if (column.fullText) { 62 hasFullTextIndex = true; 63 } 64 } 65 } 66 mProjection = projection; 67 mHasFullTextIndex = hasFullTextIndex; 68 } 69 getTableName()70 public String getTableName() { 71 return mTableName; 72 } 73 getColumnInfo()74 public ColumnInfo[] getColumnInfo() { 75 return mColumnInfo; 76 } 77 getProjection()78 public String[] getProjection() { 79 return mProjection; 80 } 81 logExecSql(SQLiteDatabase db, String sql)82 private void logExecSql(SQLiteDatabase db, String sql) { 83 // Log.i(TAG, sql); 84 db.execSQL(sql); 85 } 86 cursorToObject(Cursor cursor, Entry object)87 public void cursorToObject(Cursor cursor, Entry object) { 88 try { 89 ColumnInfo[] columns = mColumnInfo; 90 for (int i = 0, size = columns.length; i != size; ++i) { 91 ColumnInfo column = columns[i]; 92 int columnIndex = column.projectionIndex; 93 Field field = column.field; 94 switch (column.type) { 95 case TYPE_STRING: 96 field.set(object, cursor.getString(columnIndex)); 97 break; 98 case TYPE_BOOLEAN: 99 field.setBoolean(object, cursor.getShort(columnIndex) == 1); 100 break; 101 case TYPE_SHORT: 102 field.setShort(object, cursor.getShort(columnIndex)); 103 break; 104 case TYPE_INT: 105 field.setInt(object, cursor.getInt(columnIndex)); 106 break; 107 case TYPE_LONG: 108 field.setLong(object, cursor.getLong(columnIndex)); 109 break; 110 case TYPE_FLOAT: 111 field.setFloat(object, cursor.getFloat(columnIndex)); 112 break; 113 case TYPE_DOUBLE: 114 field.setDouble(object, cursor.getDouble(columnIndex)); 115 break; 116 case TYPE_BLOB: 117 field.set(object, cursor.getBlob(columnIndex)); 118 break; 119 } 120 } 121 } catch (IllegalArgumentException e) { 122 Log.e(TAG, "SchemaInfo.setFromCursor: object not of the right type"); 123 } catch (IllegalAccessException e) { 124 Log.e(TAG, "SchemaInfo.setFromCursor: field not accessible"); 125 } 126 } 127 objectToValues(Entry object, ContentValues values)128 public void objectToValues(Entry object, ContentValues values) { 129 try { 130 ColumnInfo[] columns = mColumnInfo; 131 for (int i = 0, size = columns.length; i != size; ++i) { 132 ColumnInfo column = columns[i]; 133 String columnName = column.name; 134 Field field = column.field; 135 switch (column.type) { 136 case TYPE_STRING: 137 values.put(columnName, (String) field.get(object)); 138 break; 139 case TYPE_BOOLEAN: 140 values.put(columnName, field.getBoolean(object)); 141 break; 142 case TYPE_SHORT: 143 values.put(columnName, field.getShort(object)); 144 break; 145 case TYPE_INT: 146 values.put(columnName, field.getInt(object)); 147 break; 148 case TYPE_LONG: 149 values.put(columnName, field.getLong(object)); 150 break; 151 case TYPE_FLOAT: 152 values.put(columnName, field.getFloat(object)); 153 break; 154 case TYPE_DOUBLE: 155 values.put(columnName, field.getDouble(object)); 156 break; 157 case TYPE_BLOB: 158 values.put(columnName, (byte[]) field.get(object)); 159 break; 160 } 161 } 162 } catch (IllegalArgumentException e) { 163 Log.e(TAG, "SchemaInfo.setFromCursor: object not of the right type"); 164 } catch (IllegalAccessException e) { 165 Log.e(TAG, "SchemaInfo.setFromCursor: field not accessible"); 166 } 167 } 168 queryAll(SQLiteDatabase db)169 public Cursor queryAll(SQLiteDatabase db) { 170 return db.query(mTableName, mProjection, null, null, null, null, null); 171 } 172 queryWithId(SQLiteDatabase db, long id, Entry entry)173 public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) { 174 Cursor cursor = db.query(mTableName, mProjection, "_id=?", new String[] { Long.toString(id) }, null, null, null); 175 boolean success = false; 176 if (cursor.moveToFirst()) { 177 cursorToObject(cursor, entry); 178 success = true; 179 } 180 cursor.close(); 181 return success; 182 } 183 insertOrReplace(SQLiteDatabase db, Entry entry)184 public long insertOrReplace(SQLiteDatabase db, Entry entry) { 185 ContentValues values = new ContentValues(); 186 objectToValues(entry, values); 187 if (entry.id == 0) { 188 Log.i(TAG, "removing id before insert"); 189 values.remove("_id"); 190 } 191 long id = db.replace(mTableName, "_id", values); 192 entry.id = id; 193 return id; 194 } 195 deleteWithId(SQLiteDatabase db, long id)196 public boolean deleteWithId(SQLiteDatabase db, long id) { 197 return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1; 198 } 199 createTables(SQLiteDatabase db)200 public void createTables(SQLiteDatabase db) { 201 // Wrapped class must have a @Table.Definition. 202 String tableName = mTableName; 203 if (tableName == null) { 204 return; 205 } 206 207 // Add the CREATE TABLE statement for the main table. 208 StringBuilder sql = new StringBuilder("CREATE TABLE "); 209 sql.append(tableName); 210 sql.append(" (_id INTEGER PRIMARY KEY"); 211 ColumnInfo[] columns = mColumnInfo; 212 int numColumns = columns.length; 213 for (int i = 0; i != numColumns; ++i) { 214 ColumnInfo column = columns[i]; 215 if (!column.isId()) { 216 sql.append(','); 217 sql.append(column.name); 218 sql.append(' '); 219 sql.append(SQLITE_TYPES[column.type]); 220 if (column.extraSql != null) { 221 sql.append(' '); 222 sql.append(column.extraSql); 223 } 224 } 225 } 226 sql.append(");"); 227 logExecSql(db, sql.toString()); 228 sql.setLength(0); 229 230 // Create indexes for all indexed columns. 231 for (int i = 0; i != numColumns; ++i) { 232 // Create an index on the indexed columns. 233 ColumnInfo column = columns[i]; 234 if (column.indexed) { 235 sql.append("CREATE INDEX "); 236 sql.append(tableName); 237 sql.append("_index_"); 238 sql.append(column.name); 239 sql.append(" ON "); 240 sql.append(tableName); 241 sql.append(" ("); 242 sql.append(column.name); 243 sql.append(");"); 244 logExecSql(db, sql.toString()); 245 sql.setLength(0); 246 } 247 } 248 249 if (mHasFullTextIndex) { 250 // Add an FTS virtual table if using full-text search. 251 String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX; 252 sql.append("CREATE VIRTUAL TABLE "); 253 sql.append(ftsTableName); 254 sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY"); 255 for (int i = 0; i != numColumns; ++i) { 256 ColumnInfo column = columns[i]; 257 if (column.fullText) { 258 // Add the column to the FTS table. 259 String columnName = column.name; 260 sql.append(','); 261 sql.append(columnName); 262 sql.append(" TEXT"); 263 } 264 } 265 sql.append(");"); 266 logExecSql(db, sql.toString()); 267 sql.setLength(0); 268 269 // Build an insert statement that will automatically keep the FTS 270 // table in sync. 271 StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO "); 272 insertSql.append(ftsTableName); 273 insertSql.append(" (_id"); 274 for (int i = 0; i != numColumns; ++i) { 275 ColumnInfo column = columns[i]; 276 if (column.fullText) { 277 insertSql.append(','); 278 insertSql.append(column.name); 279 } 280 } 281 insertSql.append(") VALUES (new._id"); 282 for (int i = 0; i != numColumns; ++i) { 283 ColumnInfo column = columns[i]; 284 if (column.fullText) { 285 insertSql.append(",new."); 286 insertSql.append(column.name); 287 } 288 } 289 insertSql.append(");"); 290 String insertSqlString = insertSql.toString(); 291 292 // Add an insert trigger. 293 sql.append("CREATE TRIGGER "); 294 sql.append(tableName); 295 sql.append("_insert_trigger AFTER INSERT ON "); 296 sql.append(tableName); 297 sql.append(" FOR EACH ROW BEGIN "); 298 sql.append(insertSqlString); 299 sql.append("END;"); 300 logExecSql(db, sql.toString()); 301 sql.setLength(0); 302 303 // Add an update trigger. 304 sql.append("CREATE TRIGGER "); 305 sql.append(tableName); 306 sql.append("_update_trigger AFTER UPDATE ON "); 307 sql.append(tableName); 308 sql.append(" FOR EACH ROW BEGIN "); 309 sql.append(insertSqlString); 310 sql.append("END;"); 311 logExecSql(db, sql.toString()); 312 sql.setLength(0); 313 314 // Add a delete trigger. 315 sql.append("CREATE TRIGGER "); 316 sql.append(tableName); 317 sql.append("_delete_trigger AFTER DELETE ON "); 318 sql.append(tableName); 319 sql.append(" FOR EACH ROW BEGIN DELETE FROM "); 320 sql.append(ftsTableName); 321 sql.append(" WHERE _id = old._id; END;"); 322 logExecSql(db, sql.toString()); 323 sql.setLength(0); 324 } 325 } 326 dropTables(SQLiteDatabase db)327 public void dropTables(SQLiteDatabase db) { 328 String tableName = mTableName; 329 StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS "); 330 sql.append(tableName); 331 sql.append(';'); 332 logExecSql(db, sql.toString()); 333 sql.setLength(0); 334 335 if (mHasFullTextIndex) { 336 sql.append("DROP TABLE IF EXISTS "); 337 sql.append(tableName); 338 sql.append(FULL_TEXT_INDEX_SUFFIX); 339 sql.append(';'); 340 logExecSql(db, sql.toString()); 341 } 342 343 } 344 deleteAll(SQLiteDatabase db)345 public void deleteAll(SQLiteDatabase db) { 346 StringBuilder sql = new StringBuilder("DELETE FROM "); 347 sql.append(mTableName); 348 sql.append(";"); 349 logExecSql(db, sql.toString()); 350 } 351 parseTableName(Class<? extends Object> clazz)352 private String parseTableName(Class<? extends Object> clazz) { 353 // Check for a table annotation. 354 Entry.Table table = clazz.getAnnotation(Entry.Table.class); 355 if (table == null) { 356 return null; 357 } 358 359 // Return the table name. 360 return table.value(); 361 } 362 parseColumnInfo(Class<? extends Object> clazz)363 private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) { 364 // Gather metadata from each annotated field. 365 ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>(); 366 Field[] fields = clazz.getFields(); 367 for (int i = 0; i != fields.length; ++i) { 368 // Get column metadata from the annotation. 369 Field field = fields[i]; 370 Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class); 371 if (info == null) { 372 continue; 373 } 374 375 // Determine the field type. 376 int type; 377 Class<?> fieldType = field.getType(); 378 if (fieldType == String.class) { 379 type = TYPE_STRING; 380 } else if (fieldType == boolean.class) { 381 type = TYPE_BOOLEAN; 382 } else if (fieldType == short.class) { 383 type = TYPE_SHORT; 384 } else if (fieldType == int.class) { 385 type = TYPE_INT; 386 } else if (fieldType == long.class) { 387 type = TYPE_LONG; 388 } else if (fieldType == float.class) { 389 type = TYPE_FLOAT; 390 } else if (fieldType == double.class) { 391 type = TYPE_DOUBLE; 392 } else if (fieldType == byte[].class) { 393 type = TYPE_BLOB; 394 } else { 395 throw new IllegalArgumentException("Unsupported field type for column: " + fieldType.getName()); 396 } 397 398 // Add the column to the array. 399 int index = columns.size(); 400 columns.add(new ColumnInfo(info.value(), type, info.indexed(), info.fullText(), field, index)); 401 } 402 403 // Return a list. 404 ColumnInfo[] columnList = new ColumnInfo[columns.size()]; 405 columns.toArray(columnList); 406 return columnList; 407 } 408 409 public static final class ColumnInfo { 410 public final String name; 411 public final int type; 412 public final boolean indexed; 413 public final boolean fullText; 414 public final String extraSql = ""; 415 public final Field field; 416 public final int projectionIndex; 417 ColumnInfo(String name, int type, boolean indexed, boolean fullText, Field field, int projectionIndex)418 public ColumnInfo(String name, int type, boolean indexed, boolean fullText, Field field, int projectionIndex) { 419 this.name = name.toLowerCase(); 420 this.type = type; 421 this.indexed = indexed; 422 this.fullText = fullText; 423 this.field = field; 424 this.projectionIndex = projectionIndex; 425 } 426 isId()427 public boolean isId() { 428 return name == "_id"; 429 } 430 } 431 } 432