/*
* @copyright 2012 Philip Warner
* @license GNU General Public License
*
* This file is part of Book Catalogue.
*
* Book Catalogue 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.
*
* Book Catalogue 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 Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue.database;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Date;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteCursorDriver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteQuery;
import android.graphics.Bitmap;
import com.eleybourn.bookcatalogue.CatalogueDBAdapter;
import com.eleybourn.bookcatalogue.database.DbSync.SynchronizedDb;
import com.eleybourn.bookcatalogue.database.DbSync.SynchronizedStatement;
import com.eleybourn.bookcatalogue.database.DbSync.Synchronizer;
import com.eleybourn.bookcatalogue.database.DbSync.Synchronizer.SyncLock;
import com.eleybourn.bookcatalogue.database.DbUtils.DomainDefinition;
import com.eleybourn.bookcatalogue.database.DbUtils.TableDefinition;
import com.eleybourn.bookcatalogue.utils.Logger;
import com.eleybourn.bookcatalogue.utils.StorageUtils;
import com.eleybourn.bookcatalogue.utils.TrackedCursor;
import com.eleybourn.bookcatalogue.utils.Utils;
/**
* DB Helper for Covers DB on external storage.
*
* In the initial pass, the covers database has a single table whose members are accessed via unique
* 'file names'.
*
* @author Philip Warner
*/
public class CoversDbHelper {
private static GenericOpenHelper mHelper;
private static SynchronizedDb mSharedDb;
private static boolean mSharedDbUnavailable = false;
/** Debug counter */
private static Integer mInstanceCount = 0;
/** Synchronizer to coordinate DB access. Must be STATIC so all instances share same sync */
private static final Synchronizer mSynchronizer = new Synchronizer();
/** List of statements we create so we can close them when object is closed. */
private SqlStatementManager mStatements = new SqlStatementManager();
/** DB location */
private static final String COVERS_DATABASE_NAME = StorageUtils.getSharedStoragePath() + "/covers.db";
/** DB Version */
private static final int COVERS_DATABASE_VERSION = 1;
// Domain and table definitions
/** Static Factory object to create the custom cursor */
public static final CursorFactory mTrackedCursorFactory = new CursorFactory() {
@Override
public Cursor newCursor(
SQLiteDatabase db,
SQLiteCursorDriver masterQuery,
String editTable,
SQLiteQuery query)
{
return new TrackedCursor(db, masterQuery, editTable, query, mSynchronizer);
}
};
private static class CoversHelper extends GenericOpenHelper {
public CoversHelper(String dbFilePath, CursorFactory factory, int version) {
super(dbFilePath, factory, version);
}
/**
* As with SQLiteOpenHelper, routine called to create DB
*/
@Override
public void onCreate(SQLiteDatabase db) {
DbUtils.createTables(new SynchronizedDb(db, mSynchronizer), TABLES, true );
}
/**
* As with SQLiteOpenHelper, routine called to upgrade DB
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
throw new RuntimeException("Upgrades not handled yet!");
}
}
public static final DomainDefinition DOM_ID = new DomainDefinition( "_id", "integer", "primary key autoincrement", "");
public static final DomainDefinition DOM_DATE = new DomainDefinition( "date", "datetime", "default current_timestamp", "not null");
public static final DomainDefinition DOM_TYPE = new DomainDefinition( "type", "text", "", "not null"); // T = Thumbnail; C = cover?
public static final DomainDefinition DOM_IMAGE = new DomainDefinition( "image", "blob", "", "not null");
public static final DomainDefinition DOM_WIDTH = new DomainDefinition( "width", "integer", "", "not null");
public static final DomainDefinition DOM_HEIGHT = new DomainDefinition( "height", "integer", "", "not null");
public static final DomainDefinition DOM_SIZE = new DomainDefinition( "size", "integer", "", "not null");
public static final DomainDefinition DOM_FILENAME = new DomainDefinition( "filename", "text", "", "");
public static final TableDefinition TBL_IMAGE = new TableDefinition("image", DOM_ID, DOM_TYPE, DOM_IMAGE, DOM_DATE, DOM_WIDTH, DOM_HEIGHT, DOM_SIZE, DOM_FILENAME );
static {
TBL_IMAGE
.addIndex("id", true, DOM_ID)
.addIndex("file", true, DOM_FILENAME)
.addIndex("file_date", true, DOM_FILENAME, DOM_DATE);
};
public static final TableDefinition TABLES[] = new TableDefinition[] {TBL_IMAGE};
/**
* Constructor. Fill in required fields. This is NOT based on SQLiteOpenHelper so does not need a context.
*/
public CoversDbHelper() {
if (mSharedDbUnavailable)
throw new RuntimeException("Covers database unavailable");
if (mHelper == null) {
mHelper = new CoversHelper(COVERS_DATABASE_NAME, mTrackedCursorFactory, COVERS_DATABASE_VERSION);
}
if (mSharedDb == null) {
// Try to connect.
try {
mSharedDb = new SynchronizedDb(mHelper, mSynchronizer);
} catch (Exception e) {
// Assume exception means DB corrupt. Log, rename, and retry
Logger.logError(e, "Failed to open covers db");
File f = new File(COVERS_DATABASE_NAME);
f.renameTo(new File(COVERS_DATABASE_NAME + ".dead"));
// Connect again...
try {
mSharedDb = new SynchronizedDb(mHelper, mSynchronizer);
} catch (Exception e2) {
// If we fail a second time (creating a new DB), then just give up.
mSharedDbUnavailable = true;
throw new RuntimeException("Covers database unavailable");
}
}
}
synchronized(mInstanceCount) {
mInstanceCount++;
System.out.println("CovDBA instances: " + mInstanceCount);
}
}
private SynchronizedDb getDb() {
return mSharedDb;
}
/**
* Delete the named 'file'
*
* @param filename
*/
public void deleteFile(final String filename) {
SynchronizedDb db = getDb();
SyncLock txLock = db.beginTransaction(true);
try {
db.execSQL("Drop table " + TBL_IMAGE);
DbUtils.createTables(db, new TableDefinition[] {TBL_IMAGE}, true);
db.setTransactionSuccessful();
} finally {
db.endTransaction(txLock);
}
}
/**
* Delete the cached covers associated with the passed hash
*
* @param filename
*/
private SynchronizedStatement mDeleteBookCoversStmt = null;
public void deleteBookCover(final String bookHash) {
SynchronizedDb db = getDb();
if (mDeleteBookCoversStmt == null) {
String sql = "Delete From " + TBL_IMAGE + " Where " + DOM_FILENAME + " LIKE ?";
mDeleteBookCoversStmt = mStatements.add(db, "mDeleteBookCoversStmt", sql);
}
mDeleteBookCoversStmt.bindString(1, bookHash + "%");
SyncLock txLock = db.beginTransaction(true);
try {
mDeleteBookCoversStmt.execute();
db.setTransactionSuccessful();
} finally {
db.endTransaction(txLock);
}
}
/**
* Get the named 'file'
*
* @param filename
*
* @return byte[] of image data
*/
public final byte[] getFile(final String filename, final Date lastModified) {
SynchronizedDb db = this.getDb();
Cursor c = db.query(TBL_IMAGE.getName(), new String[]{DOM_IMAGE.name}, DOM_FILENAME + "=? and " + DOM_DATE + " > ?",
new String[]{filename, Utils.toSqlDateTime(lastModified)}, null, null, null);
try {
if (!c.moveToFirst())
return null;
return c.getBlob(0);
} finally {
c.close();
}
}
/**
* Get the named 'file'
*
* @param filename
*
* @return byet[] of image data
*/
public boolean isEntryValid(String filename, Date lastModified) {
SynchronizedDb db = this.getDb();
Cursor c = db.query(TBL_IMAGE.getName(), new String[]{DOM_ID.name}, DOM_FILENAME + "=? and " + DOM_DATE + " > ?",
new String[]{filename, Utils.toSqlDateTime(lastModified)}, null, null, null);
try {
return c.moveToFirst();
} finally {
c.close();
}
}
/**
* Save the passed bitmap to a 'file'
*
* @param filename
* @param bm
*/
public void saveFile(final String filename, final Bitmap bm) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 70, out);
byte[] bytes = out.toByteArray();
saveFile(filename, bm.getHeight(), bm.getWidth(), bytes);
}
/**
* Save the passed encoded image data to a 'file'
*
* @param filename
* @param bm
*/
private SynchronizedStatement mExistsStmt = null;
public void saveFile(final String filename, final int height, final int width, final byte[] bytes) {
SynchronizedDb db = this.getDb();
if (mExistsStmt == null) {
String sql = "Select Count(" + DOM_ID + ") From " + TBL_IMAGE + " Where " + DOM_FILENAME + " = ?";
mExistsStmt = mStatements.add(db, "mExistsStmt", sql);
}
ContentValues cv = new ContentValues();
cv.put(DOM_FILENAME.name, filename);
cv.put(DOM_IMAGE.name, bytes);
cv.put(DOM_DATE.name, Utils.toSqlDateTime(new Date()));
cv.put(DOM_TYPE.name, "T");
cv.put(DOM_WIDTH.name, height);
cv.put(DOM_HEIGHT.name, width);
cv.put(DOM_SIZE.name, bytes.length);
mExistsStmt.bindString(1, filename);
long rows = 0;
SyncLock txLock = db.beginTransaction(true);
try {
if (mExistsStmt.simpleQueryForLong() == 0) {
rows = db.insert(TBL_IMAGE.getName(), null, cv);
} else {
rows = db.update(TBL_IMAGE.getName(), cv, DOM_FILENAME.name + " = ?", new String[] {filename});
}
if (rows == 0)
throw new RuntimeException("Failed to insert data");
db.setTransactionSuccessful();
} finally {
db.endTransaction(txLock);
}
}
/**
* Erase all images in the covers cache
*/
private SynchronizedStatement mEraseCoverCacheStmt = null;
public void eraseCoverCache() {
SynchronizedDb db = this.getDb();
if (mEraseCoverCacheStmt == null) {
String sql = "Delete From " + TBL_IMAGE;
mEraseCoverCacheStmt = mStatements.add(db, "mEraseCoverCacheStmt", sql);
}
mEraseCoverCacheStmt.execute();
}
/**
* Erase all cached images relating to the passed book UUID.
*
* @param uuid
*/
public int eraseCachedBookCover(String uuid) {
SynchronizedDb db = this.getDb();
// We use encodeString here because it's possible a user screws up the data and imports
// bad UUIDs...this has happened.
String sql = DOM_FILENAME + " glob '" + CatalogueDBAdapter.encodeString(uuid) + ".*'";
return db.delete(TBL_IMAGE.getName(), sql, CatalogueDBAdapter.EMPTY_STRING_ARRAY);
}
/**
* Analyze the database
*/
public void analyze() {
SynchronizedDb db = this.getDb();
String sql;
// Don't do VACUUM -- it's a complete rebuild
//sql = "vacuum";
//db.execSQL(sql);
sql = "analyze";
db.execSQL(sql);
}
public void close() {
mStatements.close();
synchronized(mInstanceCount) {
mInstanceCount--;
System.out.println("CovDBA instances: " + mInstanceCount);
}
}
}