Back to project page couchdbsyncer-android.
The source code is released under:
Apache License
If you think the Android project couchdbsyncer-android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
package au.com.team2moro.couchdbsyncer; //from w w w . j a v a 2 s . c o m import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import android.content.ContentValues; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; import android.util.Log; public class DatabaseStore { private static final String TAG = "DatabaseStore"; private DbHelper dbHelper; private Context context; private static final String[] DB_DATABASE_COLUMNS = { "_id", "name", "sequence_id", "doc_del_count", "db_name", "last_sync", "url" }; private static final String[] DB_DOCUMENT_COLUMNS = { "_id", "doc_id", "database_id", "revision", "content", "parent_id", "type", "tags" }; private static final String[] DB_ATTACHMENT_CONTENT_COLUMNS = { "_id", "document_id", "doc_id", "revision", "filename", "length", "content_type", "stale", "content" }; private static final String[] DB_ATTACHMENT_COLUMNS = { "_id", "document_id", "doc_id", "revision", "filename", "length", "content_type", "stale" }; private static final String[] DB_TYPE_COLUMN = { "type" }; public DatabaseStore(Context context) { context = context.getApplicationContext(); this.context = context; this.dbHelper = new DbHelper(context); } public DatabaseStore(Context context, String shippedPath) { this(context); // install shipped database if appropriate try { File fileDatabase = new File(getDatabasePath()); // couchdbsyncer sqlite database file long installationTime = getInstallationTime(); // time app was installed long dbModifiedTime = fileDatabase.lastModified(); Log.d(TAG, String.format("internal db: %s (modified %d), installation time %d", fileDatabase.getPath(), dbModifiedTime, installationTime)); Log.d(TAG, "new database: " + dbHelper.isNewDatabase()); if(dbHelper.isNewDatabase() || (dbModifiedTime < installationTime)) { // internal db does not exist or the shipped database (within the app) is newer installDatabase(shippedPath); } } catch (IOException e) { Log.d(TAG, "could not install shipped database", e); } } public void close() { dbHelper.close(); } /** * Delete all documents and attachments from all databases */ public void purge() { for(Database database : getDatabases()) { purge(database); } } public synchronized void purge(Database database) { purge(database, false); } /** * Delete all documents and attachments from the given database * @param database The database to purge */ public synchronized void purge(Database database, boolean deleteDatabase) { String[] whereArgs = { Long.toString(database.getDatabaseId()) }; SQLiteDatabase dbrw = this.dbHelper.getWritableDatabase(); Log.d(TAG, "deleting database: " + database.getName()); try { dbrw.beginTransaction(); dbrw.delete("attachments", "document_id IN (SELECT _id FROM documents WHERE database_id = ?)", whereArgs); dbrw.delete("documents", "database_id = ?", whereArgs); if(deleteDatabase) { dbrw.delete("databases", "_id = ?", whereArgs); } else { database.setSequenceId(0); writeDatabase(dbrw, database); } dbrw.setTransactionSuccessful(); } finally { dbrw.endTransaction(); dbrw.close(); } } public long getSequenceId(Database database) { return database.getSequenceId(); } public synchronized void updateDocument(Database database, Document document, int sequenceId) { Log.d(TAG, String.format("updating document %s (seq:%d)", document.getDocId(), sequenceId)); if(document.isDeleted()) { // delete document and attachments, updates sequence id deleteDocument(database, document); return; } // fetch existing document from database Document dbDocument = getDocument(database, document.getDocId()); Set<Attachment> oldAttachments = new HashSet<Attachment>(); Set<Attachment> updatedAttachments = new HashSet<Attachment>(); boolean newDocument = false; if(dbDocument == null) { // new document dbDocument = document; newDocument = true; } else { // existing document dbDocument.setRevision(document.getRevision()); dbDocument.setContent(document.getContent()); dbDocument.setType(document.getType()); dbDocument.setParentId(document.getParentId()); dbDocument.setTags(document.getTags()); oldAttachments.addAll(dbDocument.getAttachments()); } // go through attachments, update oldAttachments and updatedAttachments lists // set stale to true for attachments that need to be updated for(Attachment attachment : document.getAttachments()) { Attachment dbAttachment = dbDocument.getAttachment(attachment.getFilename()); oldAttachments.remove(attachment); // this attachment still exists if(newDocument || dbAttachment == null || dbAttachment.getRevision() != attachment.getRevision()) { // this attachment is not in the database yet, or is in the database but revision has changed on the server attachment.setStale(true); // mark as "stale" (needs to be updated) updatedAttachments.add(attachment); // add to download list if(dbAttachment != null) { // exists in database, set attachment id on attachment object so we know this attachment already exists attachment.setAttachmentId(dbAttachment.getAttachmentId()); } } } // write changes to database SQLiteDatabase dbrw = this.dbHelper.getWritableDatabase(); try { ContentValues values = contentValuesDocument(dbDocument); dbrw.beginTransaction(); if(document == dbDocument) { // new: insert values.put("database_id", database.getDatabaseId()); Log.d(TAG, "new document, inserting"); long documentId = dbrw.insert("documents", null, values); dbDocument.setDocumentId(documentId); } else { // existing: update Log.d(TAG, "existing document, updating"); String[] whereArgs = { Long.toString(dbDocument.getDocumentId()) }; dbrw.update("documents", values, "_id = ?", whereArgs); } // remove local attachments that are no longer attached to the document for(Attachment attachment : oldAttachments) { Log.d(TAG, "removing attachment: " + attachment.getFilename() + " documentId: " + attachment.getDocumentId()); String[] attachmentWhere = { Long.toString(attachment.getDocumentId()), attachment.getFilename() }; dbrw.delete("attachments", "document_id = ? AND filename = ?", attachmentWhere); } document.getAttachments().remove(oldAttachments); // update in-memory document state // save metadata for new/changed attachments for(Attachment attachment : updatedAttachments) { // set attachment document id (required if this is a new document, as documentId only known after document insert) boolean existing = attachment.getAttachmentId() > 0 ? true : false; attachment.setDocumentId(dbDocument.getDocumentId()); ContentValues attachmentValues = contentValuesAttachment(attachment); if(existing) { // existing attachment: update Log.d(TAG, "updating existing attachment " + attachment.getFilename()); String[] whereArgs = { Long.toString(attachment.getAttachmentId()) }; dbrw.update("attachments", attachmentValues, "_id = ?", whereArgs); } else { // new attachment: insert Log.d(TAG, "inserting new attachment " + attachment.getFilename()); attachmentValues.put("document_id", dbDocument.getDocumentId()); dbrw.insert("attachments", null, attachmentValues); } } // update sequence Id database.setSequenceId(sequenceId); writeDatabase(dbrw, database); dbrw.setTransactionSuccessful(); } finally { dbrw.endTransaction(); dbrw.close(); } } public synchronized void updateAttachment(Database database, Document document, Attachment attachment) { Log.d(TAG, "updating attachment: " + attachment.getFilename()); Attachment dbAttachment = getAttachment(attachment, false); // don't load old attachment content if(dbAttachment == null) { // attachment record should be in the database at this point Log.d(TAG, "internal error: no attachment record found for " + attachment.getFilename()); return; } dbAttachment.setStale(false); dbAttachment.setContent(attachment.getContent()); dbAttachment.setLength(attachment.getLength()); dbAttachment.setRevision(attachment.getRevision()); dbAttachment.setContentType(attachment.getContentType()); // write changes to database SQLiteDatabase dbrw = this.dbHelper.getWritableDatabase(); try { String[] whereArgs = { Long.toString(dbAttachment.getAttachmentId()) }; ContentValues attachmentValues = contentValuesAttachment(dbAttachment); dbrw.update("attachments", attachmentValues, "_id = ?", whereArgs); } finally { dbrw.close(); } } private long getCount(String selection) { SQLiteDatabase db = this.dbHelper.getReadableDatabase(); long count; try { SQLiteStatement stmt = db.compileStatement(selection); count = stmt.simpleQueryForLong(); } finally { db.close(); } return count; } /** * Count the number of documents in the given database * @return the number of documents in the database */ public long getDocumentCount(Database database) { return getCount("SELECT count(*) FROM documents WHERE database_id = " + database.getDatabaseId()); } /** * Count the number of attachments in the given database * @return the number of attachments in the database */ public long getAttachmentCount(Database database) { return getCount("SELECT count(*) FROM attachments WHERE document_id IN (SELECT _id FROM documents WHERE database_id = " + database.getDatabaseId() + ")"); } /** * Fetch a document from the database by docId (String) * @param database the database to read from * @param docId the _id of the document to fetch * @return the document, or null if the document could not be found */ public Document getDocument(Database database, String docId) { String[] selectionArgs = { Long.toString(database.getDatabaseId()), docId }; List<Document> documents = getDocuments("database_id = ? AND doc_id = ?", selectionArgs, 1); return (documents.size() == 1) ? documents.get(0) : null; } /** * Read all documents from the local database * @param database the database to read from * @return a List of Document objects */ public List<Document> getDocuments(Database database) { String[] selectionArgs = { Long.toString(database.getDatabaseId()) }; return getDocuments("database_id = ?", selectionArgs, -1); } private List<Document> getDocuments(String selection, String[] selectionArgs, int limit) { SQLiteDatabase db = this.dbHelper.getReadableDatabase(); List<Document> documents = new ArrayList<Document>(); Cursor cursor = null; try { //query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) //Query the given table, returning a Cursor over the result set. cursor = db.query("documents", DB_DOCUMENT_COLUMNS, selection, selectionArgs, null, null, null); // "_id", "doc_id", "database_id", "revision", "content", "parent_id", "type", "tags" while(cursor.moveToNext()) { String docId = cursor.getString(1); Document document = new Document(docId); document.setDocumentId(cursor.getInt(0)); document.setDatabaseId(cursor.getInt(2)); document.setRevision(cursor.getString(3)); document.setContent(cursor.getBlob(4)); // populate attachments document.setParentId(cursor.getString(5)); document.setType(cursor.getString(6)); document.setTags(cursor.getString(7)); documents.add(document); if(limit > 0 && documents.size() == limit) break; } } finally { if(cursor != null) cursor.close(); db.close(); } return documents; } /** * read documents from the local database matching the given type * @param database The database to read from * @param type the type of documents to fetch. If null, fetch documents with no type set. * @return a list of Documents */ public List<Document> getDocuments(Database database, String type) { if(type == null) { String[] selectionArgs = { Long.toString(database.getDatabaseId()) }; return getDocuments("database_id = ? AND type IS NULL", selectionArgs, -1); } else { String[] selectionArgs = { Long.toString(database.getDatabaseId()), type }; return getDocuments("database_id = ? AND type = ?", selectionArgs, -1); } } /** * read documents from the local database matching the given type and tag * @param database The database to read from * @param type the type of documents to fetch. If null, fetch documents with no type set. * @param tag A tag which is matched against the list of tags with 'LIKE'. If null, fetch documents with no tags set. * @return a list of Documents */ public List<Document> getDocuments(Database database, String type, String tag) { String selection = "database_id = ? AND " + (type != null ? "type = ? AND " : "type IS NULL AND ") + (tag != null ? "tag LIKE ?" : "tag IS NULL"); String[] selectionArgs = { Long.toString(database.getDatabaseId()), type, tag }; return getDocuments(selection, selectionArgs, -1); } /** * Find and return a list of all distinct document types in the store */ public List<String> getDocumentTypes(Database database) { SQLiteDatabase db = this.dbHelper.getReadableDatabase(); String[] selectionArgs = { Long.toString(database.getDatabaseId()) }; List<String> types = new ArrayList<String>(); Cursor cursor = null; try { cursor = db.query(true, "documents", DB_TYPE_COLUMN, "database_id = ? AND type IS NOT NULL", selectionArgs, null, null, null, null); while(cursor.moveToNext()) { types.add(cursor.getString(0)); } } finally { if(cursor != null) cursor.close(); db.close(); } return types; } /** * Delete a document and associated attachments from the local database. updates sequence id * @param document The document to delete */ private synchronized void deleteDocument(Database database, Document document) { String[] whereArgs = { Long.toString(document.getDocumentId()) }; SQLiteDatabase dbrw = this.dbHelper.getWritableDatabase(); Log.d(TAG, "deleting document: " + document.getDocumentId()); try { dbrw.beginTransaction(); dbrw.delete("documents", "_id = ?", whereArgs); dbrw.delete("attachments", "document_id = ?", whereArgs); writeDatabase(dbrw, database); dbrw.setTransactionSuccessful(); } finally { dbrw.endTransaction(); dbrw.close(); } } // read attachments from the database private List<Attachment> getAttachments(String selection, String[] selectionArgs, int limit, boolean fetchContent) { List<Attachment> attachments = new ArrayList<Attachment>(); SQLiteDatabase db = this.dbHelper.getReadableDatabase(); Cursor cursor = null; try { String[] columns = fetchContent ? DB_ATTACHMENT_CONTENT_COLUMNS : DB_ATTACHMENT_COLUMNS; cursor = db.query("attachments", columns, selection, selectionArgs, null, null, null); // "_id", "document_id", "doc_id", "revision", "filename", "length", "content_type", "stale", "content" while(cursor.moveToNext()) { Attachment attachment = new Attachment(cursor.getString(4)); attachment.setAttachmentId(cursor.getInt(0)); attachment.setDocumentId(cursor.getInt(1)); attachment.setDocId(cursor.getString(2)); attachment.setRevision(cursor.getInt(3)); attachment.setLength(cursor.getInt(5)); attachment.setContentType(cursor.getString(6)); attachment.setStale(cursor.getInt(7) == 0 ? false : true); if(fetchContent) attachment.setContent(cursor.getBlob(8)); attachments.add(attachment); if(limit > 0 && attachments.size() == limit) break; } } finally { if(cursor != null) cursor.close(); db.close(); } return attachments; } /** * Read an attachment (including content) from the local database. * @param document The document containing the attachment * @param filename The filename of the attachment to read * @return The attachment object with loaded content */ public Attachment getAttachment(Document document, String filename) { String[] selectionArgs = { Long.toString(document.getDocumentId()), filename }; List<Attachment> attachments = getAttachments("document_id = ? AND filename = ?", selectionArgs, 1, true); return attachments.size() == 1 ? attachments.get(0) : null; } private Attachment getAttachment(Attachment attachment, boolean fetchContent) { Log.d(TAG, "getAttachment: document_id = " + attachment.getDocumentId() + " filename = " + attachment.getFilename()); String[] selectionArgs = { Long.toString(attachment.getDocumentId()), attachment.getFilename() }; List<Attachment> attachments = getAttachments("document_id = ? AND filename = ?", selectionArgs, 1, fetchContent); return attachments.size() == 1 ? attachments.get(0) : null; } /** * Read an attachment (including content) from the local database. * @param attachment The attachment to read * @return The attachment object with loaded content */ public Attachment getAttachment(Attachment attachment) { return getAttachment(attachment, true); } /** * Read attachments (including content) from the local database. * @param database The database to read from * @param document The document to read attachments from * @return A list of attachments */ public List<Attachment> getAttachments(Database database, Document document) { String[] selectionArgs = { Long.toString(document.getDocumentId()) }; return getAttachments("document_id = ?", selectionArgs, -1, true); } protected List<Attachment> getStaleAttachments(Database database) { String[] selectionArgs = { Long.toString(database.getDatabaseId()) }; return getAttachments("stale = 1 AND document_id IN (SELECT _id FROM documents WHERE database_id = ?)", selectionArgs, -1, false); } /** * @return a list of all databases managed by this store */ public List<Database> getDatabases() { // fetch database records // "_id", "name", "sequence_id", "doc_del_count", "db_name", "url" SQLiteDatabase db = dbHelper.getReadableDatabase(); List<Database> databases = new ArrayList<Database>(); Cursor cursor = null; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { cursor = db.query("databases", DB_DATABASE_COLUMNS, null, null, null, null, null); while(cursor.moveToNext()) { String name = cursor.getString(1); String urlStr = cursor.getString(6); URL url = null; try { url = new URL(urlStr); } catch(MalformedURLException e) { Log.d(TAG, e.toString()); } Database database = new Database(name, url); database.setDatabaseId(cursor.getInt(0)); database.setSequenceId(cursor.getInt(2)); database.setDocDelCount(cursor.getInt(3)); database.setDbName(cursor.getString(4)); String lastSync = cursor.getString(5); if(lastSync != null) { try { database.setLastSyncDate(dateFormat.parse(lastSync)); } catch (ParseException e) { Log.e(TAG, "Parsing ISO8601 datetime failed", e); } } databases.add(database); } } finally { if(cursor != null) cursor.close(); db.close(); } return databases; } /** * return the database with the given name * @param name The name of the database to find * @return A database object, or null if the database was not found */ public Database getDatabase(String name) { for(Database database : getDatabases()) { if(database.getName().equals(name)) return database; } return null; // not found } /** * Returns the database with the given name. * If the database is not found locally, creates a new empty database with the given name and url. * The URL of the database is updated to the given url, if it differs. * @param name The name of the database * @param url The URL of the database, used in creating the database if it does not exist. * @return */ public synchronized Database getDatabase(String name, URL url) { Database database = getDatabase(name); if(database == null && url != null) { // add database database = addDatabase(name, url); } if(database != null && database.getUrl() != url) { // update database url Log.d(TAG, "updating database url to: " + url); database.setUrl(url); updateDatabase(database); } return database; } /** * Add a new database to the store * @param name The name of the new database * @param url The URL of the remote couchDB database * @return The new Database object */ public synchronized Database addDatabase(String name, URL url) { Database database = new Database(name, url); ContentValues values = contentValuesDatabase(database); SQLiteDatabase dbrw = this.dbHelper.getWritableDatabase(); long databaseId = dbrw.insert("databases", null, values); dbrw.close(); database.setDatabaseId(databaseId); return database; // re-fetch database so we have the database id } // return the path to the internal sqlite database private String getDatabasePath() { String dbPath = null; SQLiteDatabase db = null; try { db = this.dbHelper.getReadableDatabase(); dbPath = db.getPath(); } catch(Exception e) { Log.d(TAG, "error getting internal database path", e); } finally { if(db != null) db.close(); } return dbPath; } private long getInstallationTime() { try { PackageManager pm = context.getPackageManager(); ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), 0); File appFile = new File(appInfo.sourceDir); return appFile.lastModified(); } catch(PackageManager.NameNotFoundException e) { Log.d(TAG, "could not find installation time", e); } return -1; } /** * Install the database at path as the new DatabaseStore database. * The database at the given path is copied to the internal DatabaseStore path, overwriting the existing database (if it exists). * @param path The path to the sqlite database containing pre-synced data. */ public synchronized void installDatabase(String path) throws IOException { // get path to internal database String dbPath = getDatabasePath(); if(dbPath == null || path == null) return; // could not find internal path or db path is null // close existing database dbHelper.close(); this.dbHelper = null; // copy shipped database path to internal sqlite database path Log.d(TAG, "installing shipped database " + path + " to " + dbPath); InputStream myInput = context.getAssets().open(path); OutputStream myOutput = new FileOutputStream(dbPath); byte[] buffer = new byte[1024]; int length; while ((length = myInput.read(buffer)) > 0) { myOutput.write(buffer, 0, length); } myOutput.flush(); myOutput.close(); myInput.close(); // create a new dbhelper this.dbHelper = new DbHelper(this.context); } /** * Save changes to the database record to the data store. * @param database Database to save */ public synchronized void updateDatabase(Database database) { SQLiteDatabase dbrw = null; try { dbrw = this.dbHelper.getWritableDatabase(); writeDatabase(dbrw, database); } finally { if(dbrw != null) dbrw.close(); } } private void writeDatabase(SQLiteDatabase dbrw, Database database) { String[] whereArgs = { Long.toString(database.getDatabaseId()) }; ContentValues values = contentValuesDatabase(database); Log.d(TAG, "database values: " + values.toString()); dbrw.update("databases", values, "_id = ?", whereArgs); } private ContentValues contentValuesDatabase(Database database) { ContentValues values = new ContentValues(); Date lastSync = database.getLastSyncDate(); values.put("name", database.getName()); values.put("url", database.getUrl().toString()); values.put("sequence_id", database.getSequenceId()); values.put("doc_del_count", database.getDocDelCount()); values.put("db_name", database.getDbName()); if(lastSync != null) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); values.put("last_sync", dateFormat.format(lastSync)); } return values; } // { "doc_id", "database_id", "revision", "content", "object", "parent_id", "type", "tags" }; private ContentValues contentValuesDocument(Document document) { ContentValues values = new ContentValues(); values.put("doc_id", document.getDocId()); values.put("database_id", document.getDatabaseId()); values.put("revision", document.getRevision()); values.put("content", document.getContentBytes()); values.put("parent_id", document.getParentId()); values.put("type", document.getType()); values.put("tags", document.getTagsString()); return values; } // { "document_id", "doc_id", "revision", "filename", "length", "content_type", "stale", "content" }; private ContentValues contentValuesAttachment(Attachment attachment) { ContentValues values = new ContentValues(); values.put("document_id", attachment.getDocumentId()); values.put("doc_id", attachment.getDocId()); values.put("revision", attachment.getRevision()); values.put("filename", attachment.getFilename()); values.put("length", attachment.getLength()); values.put("content_type", attachment.getContentType()); values.put("stale", attachment.isStale()); values.put("content", attachment.getContent()); return values; } private class DbHelper extends SQLiteOpenHelper { static final String TAG = "DBHelper"; static final int DB_VERSION = 1; static final String DB_NAME = "couchdbsyncer.db"; private boolean newDatabase = false; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } // Called only once, first time the DB is created @Override public void onCreate(SQLiteDatabase db) { Log.d(TAG, "onCreate"); String sql = readFile("data/schema.sql"); String[] statements = sql.split(";"); for(String statement : statements) { Log.d(TAG, "onCreate sql: " + statement); db.execSQL(statement + ";"); } newDatabase = true; } public boolean isNewDatabase() { return newDatabase; } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.d(TAG, "onUpgrade"); String[] tables = {"databases", "documents", "attachments"}; for(String table : tables) { db.execSQL("drop table if exists " + table); } onCreate(db); // run onCreate to get new database } private String readFile(String s) { String result = ""; String thisLine; try { InputStream is = getClass().getResourceAsStream(s); BufferedReader br = new BufferedReader(new InputStreamReader(is)); while ((thisLine = br.readLine()) != null) { result += thisLine; } } catch (Exception e) { Log.d(TAG, e.toString()); } return result; } } }