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.android.gallery3d.common; 18 19 import android.content.ContentValues; 20 import android.database.Cursor; 21 import android.database.sqlite.SQLiteDatabase; 22 import android.text.TextUtils; 23 24 import java.lang.reflect.AnnotatedElement; 25 import java.lang.reflect.Field; 26 import java.util.ArrayList; 27 28 public final class EntrySchema { 29 @SuppressWarnings("unused") 30 private static final String TAG = "EntrySchema"; 31 32 public static final int TYPE_STRING = 0; 33 public static final int TYPE_BOOLEAN = 1; 34 public static final int TYPE_SHORT = 2; 35 public static final int TYPE_INT = 3; 36 public static final int TYPE_LONG = 4; 37 public static final int TYPE_FLOAT = 5; 38 public static final int TYPE_DOUBLE = 6; 39 public static final int TYPE_BLOB = 7; 40 private static final String SQLITE_TYPES[] = { 41 "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" }; 42 43 private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext"; 44 45 private final String mTableName; 46 private final ColumnInfo[] mColumnInfo; 47 private final String[] mProjection; 48 private final boolean mHasFullTextIndex; 49 50 public EntrySchema(Class<? extends Entry> clazz) { 51 // Get table and column metadata from reflection. 52 ColumnInfo[] columns = parseColumnInfo(clazz); 53 mTableName = parseTableName(clazz); 54 mColumnInfo = columns; 55 56 // Cache the list of projection columns and check for full-text columns. 57 String[] projection = {}; 58 boolean hasFullTextIndex = false; 59 if (columns != null) { 60 projection = new String[columns.length]; 61 for (int i = 0; i != columns.length; ++i) { 62 ColumnInfo column = columns[i]; 63 projection[i] = column.name; 64 if (column.fullText) { 65 hasFullTextIndex = true; 66 } 67 } 68 } 69 mProjection = projection; 70 mHasFullTextIndex = hasFullTextIndex; 71 } 72 73 public String getTableName() { 74 return mTableName; 75 } 76 77 public ColumnInfo[] getColumnInfo() { 78 return mColumnInfo; 79 } 80 81 public String[] getProjection() { 82 return mProjection; 83 } 84 85 public int getColumnIndex(String columnName) { 86 for (ColumnInfo column : mColumnInfo) { 87 if (column.name.equals(columnName)) { 88 return column.projectionIndex; 89 } 90 } 91 return -1; 92 } 93 94 public ColumnInfo getColumn(String columnName) { 95 int index = getColumnIndex(columnName); 96 return (index < 0) ? null : mColumnInfo[index]; 97 } 98 99 private void logExecSql(SQLiteDatabase db, String sql) { 100 db.execSQL(sql); 101 } 102 103 public <T extends Entry> T cursorToObject(Cursor cursor, T object) { 104 try { 105 for (ColumnInfo column : mColumnInfo) { 106 int columnIndex = column.projectionIndex; 107 Field field = column.field; 108 switch (column.type) { 109 case TYPE_STRING: 110 field.set(object, cursor.isNull(columnIndex) 111 ? null 112 : cursor.getString(columnIndex)); 113 break; 114 case TYPE_BOOLEAN: 115 field.setBoolean(object, cursor.getShort(columnIndex) == 1); 116 break; 117 case TYPE_SHORT: 118 field.setShort(object, cursor.getShort(columnIndex)); 119 break; 120 case TYPE_INT: 121 field.setInt(object, cursor.getInt(columnIndex)); 122 break; 123 case TYPE_LONG: 124 field.setLong(object, cursor.getLong(columnIndex)); 125 break; 126 case TYPE_FLOAT: 127 field.setFloat(object, cursor.getFloat(columnIndex)); 128 break; 129 case TYPE_DOUBLE: 130 field.setDouble(object, cursor.getDouble(columnIndex)); 131 break; 132 case TYPE_BLOB: 133 field.set(object, cursor.isNull(columnIndex) 134 ? null 135 : cursor.getBlob(columnIndex)); 136 break; 137 } 138 } 139 return object; 140 } catch (IllegalAccessException e) { 141 throw new RuntimeException(e); 142 } 143 } 144 145 private void setIfNotNull(Field field, Object object, Object value) 146 throws IllegalAccessException { 147 if (value != null) field.set(object, value); 148 } 149 150 /** 151 * Converts the ContentValues to the object. The ContentValues may not 152 * contain values for all the fields in the object. 153 */ 154 public <T extends Entry> T valuesToObject(ContentValues values, T object) { 155 try { 156 for (ColumnInfo column : mColumnInfo) { 157 String columnName = column.name; 158 Field field = column.field; 159 switch (column.type) { 160 case TYPE_STRING: 161 setIfNotNull(field, object, values.getAsString(columnName)); 162 break; 163 case TYPE_BOOLEAN: 164 setIfNotNull(field, object, values.getAsBoolean(columnName)); 165 break; 166 case TYPE_SHORT: 167 setIfNotNull(field, object, values.getAsShort(columnName)); 168 break; 169 case TYPE_INT: 170 setIfNotNull(field, object, values.getAsInteger(columnName)); 171 break; 172 case TYPE_LONG: 173 setIfNotNull(field, object, values.getAsLong(columnName)); 174 break; 175 case TYPE_FLOAT: 176 setIfNotNull(field, object, values.getAsFloat(columnName)); 177 break; 178 case TYPE_DOUBLE: 179 setIfNotNull(field, object, values.getAsDouble(columnName)); 180 break; 181 case TYPE_BLOB: 182 setIfNotNull(field, object, values.getAsByteArray(columnName)); 183 break; 184 } 185 } 186 return object; 187 } catch (IllegalAccessException e) { 188 throw new RuntimeException(e); 189 } 190 } 191 192 public void objectToValues(Entry object, ContentValues values) { 193 try { 194 for (ColumnInfo column : mColumnInfo) { 195 String columnName = column.name; 196 Field field = column.field; 197 switch (column.type) { 198 case TYPE_STRING: 199 values.put(columnName, (String) field.get(object)); 200 break; 201 case TYPE_BOOLEAN: 202 values.put(columnName, field.getBoolean(object)); 203 break; 204 case TYPE_SHORT: 205 values.put(columnName, field.getShort(object)); 206 break; 207 case TYPE_INT: 208 values.put(columnName, field.getInt(object)); 209 break; 210 case TYPE_LONG: 211 values.put(columnName, field.getLong(object)); 212 break; 213 case TYPE_FLOAT: 214 values.put(columnName, field.getFloat(object)); 215 break; 216 case TYPE_DOUBLE: 217 values.put(columnName, field.getDouble(object)); 218 break; 219 case TYPE_BLOB: 220 values.put(columnName, (byte[]) field.get(object)); 221 break; 222 } 223 } 224 } catch (IllegalAccessException e) { 225 throw new RuntimeException(e); 226 } 227 } 228 229 public String toDebugString(Entry entry) { 230 try { 231 StringBuilder sb = new StringBuilder(); 232 sb.append("ID=").append(entry.id); 233 for (ColumnInfo column : mColumnInfo) { 234 String columnName = column.name; 235 Field field = column.field; 236 Object value = field.get(entry); 237 sb.append(" ").append(columnName).append("=") 238 .append((value == null) ? "null" : value.toString()); 239 } 240 return sb.toString(); 241 } catch (IllegalAccessException e) { 242 throw new RuntimeException(e); 243 } 244 } 245 246 public String toDebugString(Entry entry, String... columnNames) { 247 try { 248 StringBuilder sb = new StringBuilder(); 249 sb.append("ID=").append(entry.id); 250 for (String columnName : columnNames) { 251 ColumnInfo column = getColumn(columnName); 252 Field field = column.field; 253 Object value = field.get(entry); 254 sb.append(" ").append(columnName).append("=") 255 .append((value == null) ? "null" : value.toString()); 256 } 257 return sb.toString(); 258 } catch (IllegalAccessException e) { 259 throw new RuntimeException(e); 260 } 261 } 262 263 public Cursor queryAll(SQLiteDatabase db) { 264 return db.query(mTableName, mProjection, null, null, null, null, null); 265 } 266 267 public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) { 268 Cursor cursor = db.query(mTableName, mProjection, "_id=?", 269 new String[] {Long.toString(id)}, null, null, null); 270 boolean success = false; 271 if (cursor.moveToFirst()) { 272 cursorToObject(cursor, entry); 273 success = true; 274 } 275 cursor.close(); 276 return success; 277 } 278 279 public long insertOrReplace(SQLiteDatabase db, Entry entry) { 280 ContentValues values = new ContentValues(); 281 objectToValues(entry, values); 282 if (entry.id == 0) { 283 values.remove("_id"); 284 } 285 long id = db.replace(mTableName, "_id", values); 286 entry.id = id; 287 return id; 288 } 289 290 public boolean deleteWithId(SQLiteDatabase db, long id) { 291 return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1; 292 } 293 294 public void createTables(SQLiteDatabase db) { 295 // Wrapped class must have a @Table.Definition. 296 String tableName = mTableName; 297 Utils.assertTrue(tableName != null); 298 299 // Add the CREATE TABLE statement for the main table. 300 StringBuilder sql = new StringBuilder("CREATE TABLE "); 301 sql.append(tableName); 302 sql.append(" (_id INTEGER PRIMARY KEY AUTOINCREMENT"); 303 for (ColumnInfo column : mColumnInfo) { 304 if (!column.isId()) { 305 sql.append(','); 306 sql.append(column.name); 307 sql.append(' '); 308 sql.append(SQLITE_TYPES[column.type]); 309 if (!TextUtils.isEmpty(column.defaultValue)) { 310 sql.append(" DEFAULT "); 311 sql.append(column.defaultValue); 312 } 313 } 314 } 315 sql.append(");"); 316 logExecSql(db, sql.toString()); 317 sql.setLength(0); 318 319 // Create indexes for all indexed columns. 320 for (ColumnInfo column : mColumnInfo) { 321 // Create an index on the indexed columns. 322 if (column.indexed) { 323 sql.append("CREATE INDEX "); 324 sql.append(tableName); 325 sql.append("_index_"); 326 sql.append(column.name); 327 sql.append(" ON "); 328 sql.append(tableName); 329 sql.append(" ("); 330 sql.append(column.name); 331 sql.append(");"); 332 logExecSql(db, sql.toString()); 333 sql.setLength(0); 334 } 335 } 336 337 if (mHasFullTextIndex) { 338 // Add an FTS virtual table if using full-text search. 339 String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX; 340 sql.append("CREATE VIRTUAL TABLE "); 341 sql.append(ftsTableName); 342 sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY"); 343 for (ColumnInfo column : mColumnInfo) { 344 if (column.fullText) { 345 // Add the column to the FTS table. 346 String columnName = column.name; 347 sql.append(','); 348 sql.append(columnName); 349 sql.append(" TEXT"); 350 } 351 } 352 sql.append(");"); 353 logExecSql(db, sql.toString()); 354 sql.setLength(0); 355 356 // Build an insert statement that will automatically keep the FTS 357 // table in sync. 358 StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO "); 359 insertSql.append(ftsTableName); 360 insertSql.append(" (_id"); 361 for (ColumnInfo column : mColumnInfo) { 362 if (column.fullText) { 363 insertSql.append(','); 364 insertSql.append(column.name); 365 } 366 } 367 insertSql.append(") VALUES (new._id"); 368 for (ColumnInfo column : mColumnInfo) { 369 if (column.fullText) { 370 insertSql.append(",new."); 371 insertSql.append(column.name); 372 } 373 } 374 insertSql.append(");"); 375 String insertSqlString = insertSql.toString(); 376 377 // Add an insert trigger. 378 sql.append("CREATE TRIGGER "); 379 sql.append(tableName); 380 sql.append("_insert_trigger AFTER INSERT ON "); 381 sql.append(tableName); 382 sql.append(" FOR EACH ROW BEGIN "); 383 sql.append(insertSqlString); 384 sql.append("END;"); 385 logExecSql(db, sql.toString()); 386 sql.setLength(0); 387 388 // Add an update trigger. 389 sql.append("CREATE TRIGGER "); 390 sql.append(tableName); 391 sql.append("_update_trigger AFTER UPDATE ON "); 392 sql.append(tableName); 393 sql.append(" FOR EACH ROW BEGIN "); 394 sql.append(insertSqlString); 395 sql.append("END;"); 396 logExecSql(db, sql.toString()); 397 sql.setLength(0); 398 399 // Add a delete trigger. 400 sql.append("CREATE TRIGGER "); 401 sql.append(tableName); 402 sql.append("_delete_trigger AFTER DELETE ON "); 403 sql.append(tableName); 404 sql.append(" FOR EACH ROW BEGIN DELETE FROM "); 405 sql.append(ftsTableName); 406 sql.append(" WHERE _id = old._id; END;"); 407 logExecSql(db, sql.toString()); 408 sql.setLength(0); 409 } 410 } 411 412 public void dropTables(SQLiteDatabase db) { 413 String tableName = mTableName; 414 StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS "); 415 sql.append(tableName); 416 sql.append(';'); 417 logExecSql(db, sql.toString()); 418 sql.setLength(0); 419 420 if (mHasFullTextIndex) { 421 sql.append("DROP TABLE IF EXISTS "); 422 sql.append(tableName); 423 sql.append(FULL_TEXT_INDEX_SUFFIX); 424 sql.append(';'); 425 logExecSql(db, sql.toString()); 426 } 427 428 } 429 430 public void deleteAll(SQLiteDatabase db) { 431 StringBuilder sql = new StringBuilder("DELETE FROM "); 432 sql.append(mTableName); 433 sql.append(";"); 434 logExecSql(db, sql.toString()); 435 } 436 437 private String parseTableName(Class<? extends Object> clazz) { 438 // Check for a table annotation. 439 Entry.Table table = clazz.getAnnotation(Entry.Table.class); 440 if (table == null) { 441 return null; 442 } 443 444 // Return the table name. 445 return table.value(); 446 } 447 448 private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) { 449 ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>(); 450 while (clazz != null) { 451 parseColumnInfo(clazz, columns); 452 clazz = clazz.getSuperclass(); 453 } 454 455 // Return a list. 456 ColumnInfo[] columnList = new ColumnInfo[columns.size()]; 457 columns.toArray(columnList); 458 return columnList; 459 } 460 461 private void parseColumnInfo(Class<? extends Object> clazz, ArrayList<ColumnInfo> columns) { 462 // Gather metadata from each annotated field. 463 Field[] fields = clazz.getDeclaredFields(); // including non-public fields 464 for (int i = 0; i != fields.length; ++i) { 465 // Get column metadata from the annotation. 466 Field field = fields[i]; 467 Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class); 468 if (info == null) continue; 469 470 // Determine the field type. 471 int type; 472 Class<?> fieldType = field.getType(); 473 if (fieldType == String.class) { 474 type = TYPE_STRING; 475 } else if (fieldType == boolean.class) { 476 type = TYPE_BOOLEAN; 477 } else if (fieldType == short.class) { 478 type = TYPE_SHORT; 479 } else if (fieldType == int.class) { 480 type = TYPE_INT; 481 } else if (fieldType == long.class) { 482 type = TYPE_LONG; 483 } else if (fieldType == float.class) { 484 type = TYPE_FLOAT; 485 } else if (fieldType == double.class) { 486 type = TYPE_DOUBLE; 487 } else if (fieldType == byte[].class) { 488 type = TYPE_BLOB; 489 } else { 490 throw new IllegalArgumentException( 491 "Unsupported field type for column: " + fieldType.getName()); 492 } 493 494 // Add the column to the array. 495 int index = columns.size(); 496 columns.add(new ColumnInfo(info.value(), type, info.indexed(), 497 info.fullText(), info.defaultValue(), field, index)); 498 } 499 } 500 501 public static final class ColumnInfo { 502 private static final String ID_KEY = "_id"; 503 504 public final String name; 505 public final int type; 506 public final boolean indexed; 507 public final boolean fullText; 508 public final String defaultValue; 509 public final Field field; 510 public final int projectionIndex; 511 512 public ColumnInfo(String name, int type, boolean indexed, 513 boolean fullText, String defaultValue, Field field, int projectionIndex) { 514 this.name = name.toLowerCase(); 515 this.type = type; 516 this.indexed = indexed; 517 this.fullText = fullText; 518 this.defaultValue = defaultValue; 519 this.field = field; 520 this.projectionIndex = projectionIndex; 521 522 field.setAccessible(true); // in order to set non-public fields 523 } 524 525 public boolean isId() { 526 return ID_KEY.equals(name); 527 } 528 } 529 } 530