From e2aca3790e32ea8273d43de567744c70c5806828 Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Sun, 31 Mar 2013 10:39:16 +0530 Subject: [PATCH 01/11] Prevented NPE in TDPuller if changeTrackerClient is not created before DB needs to stop. Added schema for replicator_log table --- .../src/com/couchbase/touchdb/TDDatabase.java | 7 +++++++ .../src/com/couchbase/touchdb/replicator/TDPuller.java | 10 +++++++--- .../com/couchbase/touchdb/replicator/TDReplicator.java | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index 986a265..ff4cb39 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -137,6 +137,13 @@ public enum TDContentOptions { " push BOOLEAN, " + " last_sequence TEXT, " + " UNIQUE (remote, push)); " + + " CREATE TABLE replicator_log ( " + + " remote TEXT NOT NULL, " + + " push BOOLEAN, " + + " docid TEXT NOT NULL, " + + " revid TEXT NOT NULL, " + + " sequence TEXT, " + + " UNIQUE (remote, push, docid, revid)); " + " PRAGMA user_version = 3"; // at the end, update user_version /*************************************************************************************************/ diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index d534bc6..c921f32 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -79,9 +79,13 @@ public void stop() { return; } - changeTracker.setClient(null); // stop it from calling my changeTrackerStopped() - changeTracker.stop(); - changeTracker = null; + // Prevents NPE in the event, the app is closed before the replication + // could even start properly + if(changeTracker != null) { + changeTracker.setClient(null); // stop it from calling my changeTrackerStopped() + changeTracker.stop(); + changeTracker = null; + } synchronized(this) { revsToPull = null; diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index 39977f8..07adfa1 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -221,7 +221,7 @@ public void processInbox(TDRevisionList inbox) { } public void sendAsyncRequest(String method, String relativePath, Object body, TDRemoteRequestCompletionBlock onCompletion) { - //Log.v(TDDatabase.TAG, String.format("%s: %s .%s", toString(), method, relativePath)); + Log.v(TDDatabase.TAG, String.format("%s: %s .%s", toString(), method, relativePath)); String urlStr = remote.toExternalForm() + relativePath; try { URL url = new URL(urlStr); From a2032dea6cf30be5619d594d641119456ca30b1b Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Sun, 31 Mar 2013 16:44:58 +0530 Subject: [PATCH 02/11] Added a db log to log every revision while replicating. So as to ensure that we never lose any data. The sequence is updated immediately (as changes are received) and pusher/puller eventually ensures that we have a consistant db --- .../src/com/couchbase/touchdb/TDDatabase.java | 53 ++++++++++- .../touchdb/replicator/TDPuller.java | 86 +++++++++++------- .../touchdb/replicator/TDPusher.java | 34 ++++++- .../touchdb/replicator/TDReplicator.java | 88 ++++++++++++++----- 4 files changed, 198 insertions(+), 63 deletions(-) diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index ff4cb39..569c667 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -34,6 +34,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; +import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.os.Handler; @@ -139,10 +140,11 @@ public enum TDContentOptions { " UNIQUE (remote, push)); " + " CREATE TABLE replicator_log ( " + " remote TEXT NOT NULL, " + - " push BOOLEAN, " + + " push BOOLEAN, " + " docid TEXT NOT NULL, " + " revid TEXT NOT NULL, " + - " sequence TEXT, " + + " deleted BOOLEAN, " + + " sequence INTEGER, " + " UNIQUE (remote, push, docid, revid)); " + " PRAGMA user_version = 3"; // at the end, update user_version @@ -2281,6 +2283,53 @@ public TDReplicator getReplicator(URL remote, HttpClientFactory httpClientFactor activeReplicators.add(result); return result; } + + public TDRevisionList getPendingRevisions(URL url, boolean push){ + TDRevisionList result = new TDRevisionList(); + String[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0) }; + Cursor cursor = database.rawQuery("SELECT docid, revid, deleted, sequence FROM replicator_log WHERE remote=? AND push=? LIMIT 50", args); + if(cursor.moveToFirst()) { + do{ + TDRevision rev = new TDRevision(cursor.getString(0), cursor.getString(1), cursor.getInt(2) == 1? true : false); + rev.setSequence(cursor.getLong(3)); + result.add(rev); + } while(cursor.moveToNext()); + } + return result; + } + + public boolean logRevision(URL url, boolean push, TDRevision rev){ + boolean success = false; + Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), rev.getDocId(), rev.getRevId(), (rev.isDeleted()?1:0), rev.getSequence()}; + try { + database.execSQL("INSERT INTO replicator_log(remote, push, docid, revid, deleted, sequence) VALUES(?,?,?,?,?,?)", args); + Cursor cursor = database.rawQuery("SELECT changes()", null); + if(cursor.moveToFirst() && cursor.getInt(0)>0){ + success = true; + } + }catch(SQLiteConstraintException e){ + // Trying to log a revision that is already present + // Do nothing + success = true; + } + return success; + } + + public void removeLogForRevision(URL url, boolean push, TDRevision rev){ + Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), rev.getDocId(), rev.getRevId()}; + try { + database.execSQL("DELETE FROM replicator_log WHERE remote=? AND push=? AND docid=? AND revid=?", args); + Cursor cursor = database.rawQuery("SELECT changes()", null); + if(cursor.moveToFirst() && cursor.getInt(0)>0){ + //success = true; + } + }catch(SQLiteConstraintException e){ + // Trying to log a revision that is already present + // Do nothing + //success = true; + } + //return success; + } public String lastSequenceWithRemoteURL(URL url, boolean push) { Cursor cursor = null; diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index c921f32..c89c632 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -92,7 +92,7 @@ public void stop() { } super.stop(); - + downloadsToInsert.flush(); } @@ -119,7 +119,8 @@ public void changeTrackerReceivedChange(Map change) { } boolean deleted = (change.containsKey("deleted") && ((Boolean)change.get("deleted")).equals(Boolean.TRUE)); List> changes = (List>)change.get("changes"); - for (Map changeDict : changes) { + ArrayList revs = new ArrayList(); + for (Map changeDict : changes) { String revID = (String)changeDict.get("rev"); if(revID == null) { continue; @@ -127,9 +128,19 @@ public void changeTrackerReceivedChange(Map change) { TDPulledRevision rev = new TDPulledRevision(docID, revID, deleted); rev.setRemoteSequenceID(lastSequence); rev.setSequence(++nextFakeSequence); - addToInbox(rev); + //addToInbox(rev); + revs .add(rev); + } + if(logRevisions(revs)){ + setChangesTotal(getChangesTotal() + changes.size()); + + // We set the sequence to ensure that changes tracker keeps + // moving forward. The docs pull eventually catches up. Filters + // are quite slow on CouchDB if you are pulling changes + // from the beginning, we want to retain as much progress we have + // made as possible + setLastSequence(lastSequence); } - setChangesTotal(getChangesTotal() + changes.size()); } @Override @@ -140,16 +151,16 @@ public void changeTrackerStopped(TDChangeTracker tracker) { // error = tracker.getError(); // } changeTracker = null; - if(batcher != null) { - batcher.flush(); - } +// if(batcher != null) { +// batcher.flush(); +// } asyncTaskFinished(1); } @Override public HttpClient getHttpClient() { - HttpClient httpClient = this.clientFacotry.getHttpClient(); + HttpClient httpClient = this.clientFactory.getHttpClient(); return httpClient; } @@ -161,25 +172,29 @@ public HttpClient getHttpClient() { public void processInbox(TDRevisionList inbox) { // Ask the local database which of the revs are not known to it: //Log.w(TDDatabase.TAG, String.format("%s: Looking up %s", this, inbox)); - String lastInboxSequence = ((TDPulledRevision)inbox.get(inbox.size()-1)).getRemoteSequenceID(); + // We have already updated the sequence + //String lastInboxSequence = ((TDPulledRevision)inbox.get(inbox.size()-1)).getRemoteSequenceID(); int total = getChangesTotal() - inbox.size(); - if(!db.findMissingRevisions(inbox)) { - Log.w(TDDatabase.TAG, String.format("%s failed to look up local revs", this)); - inbox = null; - } + + // Seems to use up a lot of memory + // if(!db.findMissingRevisions(inbox)) { + // Log.w(TDDatabase.TAG, + // String.format("%s failed to look up local revs", this)); + // inbox = null; + // } //introducing this to java version since inbox may now be null everywhere int inboxCount = 0; if(inbox != null) { inboxCount = inbox.size(); } - if(getChangesTotal() != total + inboxCount) { - setChangesTotal(total + inboxCount); - } +// if(getChangesTotal() != total + inboxCount) { +// setChangesTotal(total + inboxCount); +// } if(inboxCount == 0) { // Nothing to do. Just bump the lastSequence. Log.w(TDDatabase.TAG, String.format("%s no new remote revisions to fetch", this)); - setLastSequence(lastInboxSequence); + // setLastSequence(lastInboxSequence); return; } @@ -196,14 +211,14 @@ public void processInbox(TDRevisionList inbox) { //TEST //adding wait here to prevent revsToPull from getting too large - while(revsToPull != null && revsToPull.size() > 1000) { - pullRemoteRevisions(); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - //wake up - } - } + // while(revsToPull != null && revsToPull.size() > 1000) { + // pullRemoteRevisions(); + // try { + // Thread.sleep(500); + // } catch (InterruptedException e) { + // //wake up + // } + // } } /** @@ -230,7 +245,7 @@ public void pullRemoteRevision(final TDRevision rev) { // Construct a query. We want the revision history, and the bodies of attachments that have // been added since the latest revisions we have locally. // See: http://wiki.apache.org/couchdb/HTTP_Document_API#Getting_Attachments_With_a_Document - StringBuilder path = new StringBuilder("/" + URLEncoder.encode(rev.getDocId()) + "?rev=" + URLEncoder.encode(rev.getRevId()) + "&revs=true&attachments=true"); + StringBuilder path = new StringBuilder("/" + rev.getDocId() + "?rev=" + URLEncoder.encode(rev.getRevId()) + "&revs=true&attachments=true"); List knownRevs = knownCurrentRevIDs(rev); if(knownRevs == null) { //this means something is wrong, possibly the replicator has shut down @@ -303,7 +318,7 @@ public int compare(List list1, List list2) { }); boolean allGood = true; - TDPulledRevision lastGoodRev = null; + //TDPulledRevision lastGoodRev = null; if(db == null) { return; @@ -312,7 +327,7 @@ public int compare(List list1, List list2) { boolean success = false; try { for (List revAndHistory : revs) { - TDPulledRevision rev = (TDPulledRevision)revAndHistory.get(0); + TDRevision rev = (TDRevision) revAndHistory.get(0); List history = (List)revAndHistory.get(1); // Insert the revision: TDStatus status = db.forceInsert(rev, history, remote); @@ -325,18 +340,21 @@ public int compare(List list1, List list2) { allGood = false; // stop advancing lastGoodRev } } + + // Remove the replicator_log entry for the remote,push,docid,rev + removeLogForRevision(rev); if(allGood) { - lastGoodRev = rev; + //lastGoodRev = rev; } } // Now update lastSequence from the latest consecutively inserted revision: - long lastGoodFakeSequence = lastGoodRev.getSequence(); - if(lastGoodFakeSequence > maxInsertedFakeSequence) { - maxInsertedFakeSequence = lastGoodFakeSequence; - setLastSequence(lastGoodRev.getRemoteSequenceID()); - } + // long lastGoodFakeSequence = lastGoodRev.getSequence(); + // if(lastGoodFakeSequence > maxInsertedFakeSequence) { + // maxInsertedFakeSequence = lastGoodFakeSequence; + // setLastSequence(lastGoodRev.getRemoteSequenceID()); + // } Log.w(TDDatabase.TAG, this + " finished inserting " + revs.size() + " revisions"); success = true; diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java index dd93880..7a6468e 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java @@ -96,7 +96,12 @@ public void beginReplicating() { } TDRevisionList changes = db.changesSince(lastSequenceLong, null, filter); if(changes.size() > 0) { - processInbox(changes); + // Write these changes + //processInbox(changes); + if(logRevisions(changes)){ + long lastSeq = changes.get(changes.size()-1).getSequence(); + setLastSequence(String.format("%d", lastSeq)); + } } // Now listen for future changes (in continuous mode): @@ -133,7 +138,12 @@ public void update(Observable observable, Object data) { } TDRevision rev = (TDRevision)change.get("rev"); if(rev != null && ((filter == null) || filter.filter(rev))) { - addToInbox(rev); + //addToInbox(rev); + + // We add it to the log and we move the counter up + if(logRevision(rev)){ + setLastSequence(String.format("%d", rev.getSequence())); + } } } @@ -141,6 +151,10 @@ public void update(Observable observable, Object data) { @Override public void processInbox(final TDRevisionList inbox) { + if(inbox.size() == 0){ + return; + } + final long lastInboxSequence = inbox.get(inbox.size()-1).getSequence(); // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants: Map> diffs = new HashMap>(); @@ -207,6 +221,7 @@ public void onCompletion(Object response, Throwable e) { Map bulkDocsBody = new HashMap(); bulkDocsBody.put("docs", docsToSend); bulkDocsBody.put("new_edits", false); + bulkDocsBody.put("all_or_nothing", true); Log.i(TDDatabase.TAG, String.format("%s: Sending %d revisions", this, numDocsToSend)); Log.v(TDDatabase.TAG, String.format("%s: Sending %s", this, inbox)); setChangesTotal(getChangesTotal() + numDocsToSend); @@ -219,7 +234,12 @@ public void onCompletion(Object result, Throwable e) { error = e; } else { Log.v(TDDatabase.TAG, String.format("%s: Sent %s", this, inbox)); - setLastSequence(String.format("%d", lastInboxSequence)); + //setLastSequence(String.format("%d", lastInboxSequence)); + db.beginTransaction(); + for(TDRevision rev : inbox) { + removeLogForRevision(rev); + } + db.endTransaction(true); } setChangesProcessed(getChangesProcessed() + numDocsToSend); asyncTaskFinished(1); @@ -228,7 +248,13 @@ public void onCompletion(Object result, Throwable e) { } else { // If none of the revisions are new to the remote, just bump the lastSequence: - setLastSequence(String.format("%d", lastInboxSequence)); + //setLastSequence(String.format("%d", lastInboxSequence)); + // Remove entries from replicator_log + db.beginTransaction(); + for(TDRevision rev : inbox) { + removeLogForRevision(rev); + } + db.endTransaction(true); } asyncTaskFinished(1); } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index 07adfa1..0ce27fa 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -2,6 +2,7 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -19,8 +20,6 @@ import com.couchbase.touchdb.TDRevision; import com.couchbase.touchdb.TDRevisionList; import com.couchbase.touchdb.support.HttpClientFactory; -import com.couchbase.touchdb.support.TDBatchProcessor; -import com.couchbase.touchdb.support.TDBatcher; import com.couchbase.touchdb.support.TDRemoteRequest; import com.couchbase.touchdb.support.TDRemoteRequestCompletionBlock; @@ -41,16 +40,28 @@ public abstract class TDReplicator extends Observable { protected boolean active; protected Throwable error; protected String sessionID; - protected TDBatcher batcher; + //protected TDBatcher batcher; protected int asyncTaskCount; private int changesProcessed; private int changesTotal; - protected final HttpClientFactory clientFacotry; + protected final HttpClientFactory clientFactory; protected String filterName; protected Map filterParams; + protected PendingChanges pendingChanges; protected static final int PROCESSOR_DELAY = 500; protected static final int INBOX_CAPACITY = 100; + + private class PendingChanges implements Runnable { + + @Override + public void run() { + TDRevisionList inbox = TDReplicator.this.db.getPendingRevisions(getRemote(), + isPush()); + TDReplicator.this.processInbox(inbox); + TDReplicator.this.handler.postDelayed(pendingChanges, PROCESSOR_DELAY*2*5); + } + }; public TDReplicator(TDDatabase db, URL remote, boolean continuous) { this(db, remote, continuous, null); @@ -63,18 +74,24 @@ public TDReplicator(TDDatabase db, URL remote, boolean continuous, HttpClientFac this.continuous = continuous; this.handler = db.getHandler(); - - batcher = new TDBatcher(db.getHandler(), INBOX_CAPACITY, PROCESSOR_DELAY, new TDBatchProcessor() { - @Override - public void process(List inbox) { - Log.v(TDDatabase.TAG, "*** " + toString() + ": BEGIN processInbox (" + inbox.size() + " sequences)"); - processInbox(new TDRevisionList(inbox)); - Log.v(TDDatabase.TAG, "*** " + toString() + ": END processInbox (lastSequence=" + lastSequence); - active = false; - } - }); - - this.clientFacotry = clientFacotry != null ? clientFacotry : new HttpClientFactory() { + // batcher = new TDBatcher(db.getHandler(), INBOX_CAPACITY, + // PROCESSOR_DELAY, new TDBatchProcessor() { + // @Override + // public void process(List inbox) { + // inbox = TDReplicator.this.db.getPendingRevisions(getRemote(), + // isPush()); + // Log.v(TDDatabase.TAG, "*** " + toString() + ": BEGIN processInbox (" + // + inbox.size() + " sequences)"); + // processInbox(new TDRevisionList(inbox)); + // Log.v(TDDatabase.TAG, "*** " + toString() + + // ": END processInbox (lastSequence=" + lastSequence); + // active = false; + // } + // }); + + this.handler.postDelayed(pendingChanges = new PendingChanges(), PROCESSOR_DELAY); + + this.clientFactory = clientFacotry != null ? clientFacotry : new HttpClientFactory() { @Override public HttpClient getHttpClient() { return new DefaultHttpClient(); @@ -99,6 +116,9 @@ public URL getRemote() { } public void databaseClosing() { + if(pendingChanges != null){ + this.handler.removeCallbacks(pendingChanges); + } saveLastSequence(); stop(); db = null; @@ -179,7 +199,7 @@ public void stop() { return; } Log.v(TDDatabase.TAG, toString() + " STOPPING..."); - batcher.flush(); + //batcher.flush(); continuous = false; if(asyncTaskCount == 0) { stopped(); @@ -193,7 +213,7 @@ public void stopped() { saveLastSequence(); - batcher = null; + //batcher = null; db = null; } @@ -209,23 +229,45 @@ public synchronized void asyncTaskFinished(int numTasks) { } public void addToInbox(TDRevision rev) { - if(batcher.count() == 0) { - active = true; - } - batcher.queueObject(rev); + // if(batcher.count() == 0) { + // active = true; + // } + // batcher.queueObject(rev); //Log.v(TDDatabase.TAG, String.format("%s: Received #%d %s", toString(), rev.getSequence(), rev.toString())); } public void processInbox(TDRevisionList inbox) { } + + public boolean logRevisions(ArrayList revs){ + this.db.beginTransaction(); + boolean success = true; + for(int i =0; i < revs.size() && success; i++){ + success = success && logRevision(revs.get(i)); + } + this.db.endTransaction(success); + return success; + } + + public boolean logRevision(TDRevision rev){ + return this.db.logRevision(this.remote, isPush(), rev); + } + + public List getPendingRevisions(){ + return this.db.getPendingRevisions(this.remote, isPush()); + } + + public void removeLogForRevision(TDRevision rev){ + this.db.removeLogForRevision(this.remote, isPush(), rev); + } public void sendAsyncRequest(String method, String relativePath, Object body, TDRemoteRequestCompletionBlock onCompletion) { Log.v(TDDatabase.TAG, String.format("%s: %s .%s", toString(), method, relativePath)); String urlStr = remote.toExternalForm() + relativePath; try { URL url = new URL(urlStr); - TDRemoteRequest request = new TDRemoteRequest(db.getHandler(), clientFacotry, method, url, body, onCompletion); + TDRemoteRequest request = new TDRemoteRequest(db.getHandler(), clientFactory, method, url, body, onCompletion); request.start(); } catch (MalformedURLException e) { Log.e(TDDatabase.TAG, "Malformed URL for async request", e); From f3747ab53ccd7d82040e7fcb4de45e7fc6308f1b Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Tue, 2 Apr 2013 07:55:09 +0530 Subject: [PATCH 03/11] Added the cursor.close() that got left behind in the previous commit --- .../src/com/couchbase/touchdb/TDDatabase.java | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index 569c667..264ccf2 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -138,14 +138,6 @@ public enum TDContentOptions { " push BOOLEAN, " + " last_sequence TEXT, " + " UNIQUE (remote, push)); " + - " CREATE TABLE replicator_log ( " + - " remote TEXT NOT NULL, " + - " push BOOLEAN, " + - " docid TEXT NOT NULL, " + - " revid TEXT NOT NULL, " + - " deleted BOOLEAN, " + - " sequence INTEGER, " + - " UNIQUE (remote, push, docid, revid)); " + " PRAGMA user_version = 3"; // at the end, update user_version /*************************************************************************************************/ @@ -312,6 +304,22 @@ public boolean open() { return false; } } + + if (dbVersion < 5) { + String upgradeSql = "CREATE TABLE replicator_log ( " + + " remote TEXT NOT NULL, " + + " push BOOLEAN, " + + " docid TEXT NOT NULL, " + + " revid TEXT NOT NULL, " + + " deleted BOOLEAN, " + + " sequence INTEGER, " + + " UNIQUE (remote, push, docid, revid)); " + + "PRAGMA user_version = 5"; + if(!initialize(upgradeSql)) { + database.close(); + return false; + } + } try { attachments = new TDBlobStore(getAttachmentStorePath()); @@ -2295,15 +2303,20 @@ public TDRevisionList getPendingRevisions(URL url, boolean push){ result.add(rev); } while(cursor.moveToNext()); } + + if(cursor != null) { + cursor.close(); + } return result; } public boolean logRevision(URL url, boolean push, TDRevision rev){ boolean success = false; Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), rev.getDocId(), rev.getRevId(), (rev.isDeleted()?1:0), rev.getSequence()}; - try { + Cursor cursor = null; + try { database.execSQL("INSERT INTO replicator_log(remote, push, docid, revid, deleted, sequence) VALUES(?,?,?,?,?,?)", args); - Cursor cursor = database.rawQuery("SELECT changes()", null); + cursor = database.rawQuery("SELECT changes()", null); if(cursor.moveToFirst() && cursor.getInt(0)>0){ success = true; } @@ -2311,15 +2324,20 @@ public boolean logRevision(URL url, boolean push, TDRevision rev){ // Trying to log a revision that is already present // Do nothing success = true; + } finally { + if(cursor != null) { + cursor.close(); + } } return success; } public void removeLogForRevision(URL url, boolean push, TDRevision rev){ Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), rev.getDocId(), rev.getRevId()}; - try { + Cursor cursor = null; + try { database.execSQL("DELETE FROM replicator_log WHERE remote=? AND push=? AND docid=? AND revid=?", args); - Cursor cursor = database.rawQuery("SELECT changes()", null); + cursor = database.rawQuery("SELECT changes()", null); if(cursor.moveToFirst() && cursor.getInt(0)>0){ //success = true; } @@ -2327,6 +2345,10 @@ public void removeLogForRevision(URL url, boolean push, TDRevision rev){ // Trying to log a revision that is already present // Do nothing //success = true; + } finally { + if(cursor != null) { + cursor.close(); + } } //return success; } From bd0118107b9c8a59cd39a2bfbab77ab1e6618a7e Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Wed, 10 Apr 2013 11:02:17 +0530 Subject: [PATCH 04/11] Reverted to include URL encode for doc id --- .../src/com/couchbase/touchdb/replicator/TDPuller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index c89c632..a1378b4 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -245,7 +245,7 @@ public void pullRemoteRevision(final TDRevision rev) { // Construct a query. We want the revision history, and the bodies of attachments that have // been added since the latest revisions we have locally. // See: http://wiki.apache.org/couchdb/HTTP_Document_API#Getting_Attachments_With_a_Document - StringBuilder path = new StringBuilder("/" + rev.getDocId() + "?rev=" + URLEncoder.encode(rev.getRevId()) + "&revs=true&attachments=true"); + StringBuilder path = new StringBuilder("/" + URLEncoder.encode(rev.getDocId()) + "?rev=" + URLEncoder.encode(rev.getRevId()) + "&revs=true&attachments=true"); List knownRevs = knownCurrentRevIDs(rev); if(knownRevs == null) { //this means something is wrong, possibly the replicator has shut down From f1945c74bff25cead3be9e339ae94284a60561fc Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Mon, 6 May 2013 13:04:37 +0530 Subject: [PATCH 05/11] Fixed NPE TDReplicator --- .../touchdb/replicator/TDReplicator.java | 690 +++++++++--------- 1 file changed, 362 insertions(+), 328 deletions(-) diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index 0ce27fa..de71b11 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -25,56 +25,60 @@ public abstract class TDReplicator extends Observable { - private static int lastSessionID = 0; - - protected Handler handler; - protected TDDatabase db; - protected URL remote; - protected boolean continuous; - protected String lastSequence; - protected boolean lastSequenceChanged; - protected Map remoteCheckpoint; - protected boolean savingCheckpoint; - protected boolean overdueForSave; - protected boolean running; - protected boolean active; - protected Throwable error; - protected String sessionID; - //protected TDBatcher batcher; - protected int asyncTaskCount; - private int changesProcessed; - private int changesTotal; - protected final HttpClientFactory clientFactory; - protected String filterName; - protected Map filterParams; - protected PendingChanges pendingChanges; - - protected static final int PROCESSOR_DELAY = 500; - protected static final int INBOX_CAPACITY = 100; - - private class PendingChanges implements Runnable { - + private static int lastSessionID = 0; + + protected Handler handler; + protected TDDatabase db; + protected URL remote; + protected boolean continuous; + protected String lastSequence; + protected boolean lastSequenceChanged; + protected Map remoteCheckpoint; + protected boolean savingCheckpoint; + protected boolean overdueForSave; + protected boolean running; + protected boolean active; + protected Throwable error; + protected String sessionID; + // protected TDBatcher batcher; + protected int asyncTaskCount; + private int changesProcessed; + private int changesTotal; + protected final HttpClientFactory clientFactory; + protected String filterName; + protected Map filterParams; + protected PendingChanges pendingChanges; + + protected static final int PROCESSOR_DELAY = 500; + protected static final int INBOX_CAPACITY = 100; + + private class PendingChanges implements Runnable { + @Override public void run() { - TDRevisionList inbox = TDReplicator.this.db.getPendingRevisions(getRemote(), - isPush()); - TDReplicator.this.processInbox(inbox); - TDReplicator.this.handler.postDelayed(pendingChanges, PROCESSOR_DELAY*2*5); + if (TDReplicator.this.db != null) { + TDRevisionList inbox = TDReplicator.this.db + .getPendingRevisions(getRemote(), isPush()); + TDReplicator.this.processInbox(inbox); + } + TDReplicator.this.handler.postDelayed(pendingChanges, + PROCESSOR_DELAY * 2 * 5); } }; - public TDReplicator(TDDatabase db, URL remote, boolean continuous) { - this(db, remote, continuous, null); - } + public TDReplicator(TDDatabase db, URL remote, boolean continuous) { + this(db, remote, continuous, null); + } - public TDReplicator(TDDatabase db, URL remote, boolean continuous, HttpClientFactory clientFacotry) { + public TDReplicator(TDDatabase db, URL remote, boolean continuous, + HttpClientFactory clientFacotry) { - this.db = db; - this.remote = remote; - this.continuous = continuous; - this.handler = db.getHandler(); + this.db = db; + this.remote = remote; + this.continuous = continuous; + this.handler = db.getHandler(); - // batcher = new TDBatcher(db.getHandler(), INBOX_CAPACITY, + // batcher = new TDBatcher(db.getHandler(), INBOX_CAPACITY, // PROCESSOR_DELAY, new TDBatchProcessor() { // @Override // public void process(List inbox) { @@ -88,297 +92,327 @@ public TDReplicator(TDDatabase db, URL remote, boolean continuous, HttpClientFac // active = false; // } // }); - - this.handler.postDelayed(pendingChanges = new PendingChanges(), PROCESSOR_DELAY); - this.clientFactory = clientFacotry != null ? clientFacotry : new HttpClientFactory() { - @Override - public HttpClient getHttpClient() { - return new DefaultHttpClient(); + this.handler.postDelayed(pendingChanges = new PendingChanges(), + PROCESSOR_DELAY); + + this.clientFactory = clientFacotry != null ? clientFacotry + : new HttpClientFactory() { + @Override + public HttpClient getHttpClient() { + return new DefaultHttpClient(); + } + }; + } + + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + public void setFilterParams(Map filterParams) { + this.filterParams = filterParams; + } + + public boolean isRunning() { + return running; + } + + public URL getRemote() { + return remote; + } + + public void databaseClosing() { + if (pendingChanges != null) { + this.handler.removeCallbacks(pendingChanges); + } + saveLastSequence(); + stop(); + db = null; + } + + public String toString() { + String maskedRemoteWithoutCredentials = (remote != null ? remote + .toExternalForm() : ""); + maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials + .replaceAll("://.*:.*@", "://---:---@"); + String name = getClass().getSimpleName() + "[" + + maskedRemoteWithoutCredentials + "]"; + return name; + } + + public boolean isPush() { + return false; + } + + public String getLastSequence() { + return lastSequence; + } + + public void setLastSequence(String lastSequenceIn) { + if (!lastSequenceIn.equals(lastSequence)) { + Log.v(TDDatabase.TAG, toString() + ": Setting lastSequence to " + + lastSequenceIn + " from( " + lastSequence + ")"); + lastSequence = lastSequenceIn; + if (!lastSequenceChanged) { + lastSequenceChanged = true; + handler.postDelayed(new Runnable() { + + @Override + public void run() { + saveLastSequence(); + } + }, 2 * 1000); } - }; - } - - public void setFilterName(String filterName) { - this.filterName = filterName; - } - - public void setFilterParams(Map filterParams) { - this.filterParams = filterParams; - } - - public boolean isRunning() { - return running; - } - - public URL getRemote() { - return remote; - } - - public void databaseClosing() { - if(pendingChanges != null){ - this.handler.removeCallbacks(pendingChanges); - } - saveLastSequence(); - stop(); - db = null; - } - - public String toString() { - String maskedRemoteWithoutCredentials = (remote != null ? remote.toExternalForm() : ""); - maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials.replaceAll("://.*:.*@","://---:---@"); - String name = getClass().getSimpleName() + "[" + maskedRemoteWithoutCredentials + "]"; - return name; - } - - public boolean isPush() { - return false; - } - - public String getLastSequence() { - return lastSequence; - } - - public void setLastSequence(String lastSequenceIn) { - if(!lastSequenceIn.equals(lastSequence)) { - Log.v(TDDatabase.TAG, toString() + ": Setting lastSequence to " + lastSequenceIn + " from( " + lastSequence + ")"); - lastSequence = lastSequenceIn; - if(!lastSequenceChanged) { - lastSequenceChanged = true; - handler.postDelayed(new Runnable() { - - @Override - public void run() { - saveLastSequence(); - } - }, 2 * 1000); - } - } - } - - public int getChangesProcessed() { - return changesProcessed; - } - - public void setChangesProcessed(int processed) { - this.changesProcessed = processed; - setChanged(); - notifyObservers(); - } - - public int getChangesTotal() { - return changesTotal; - } - - public void setChangesTotal(int total) { - this.changesTotal = total; - setChanged(); - notifyObservers(); - } - - public String getSessionID() { - return sessionID; - } - - public void start() { - if(running) { - return; - } - this.sessionID = String.format("repl%03d", ++lastSessionID); - Log.v(TDDatabase.TAG, toString() + " STARTING ..."); - running = true; - lastSequence = null; - - fetchRemoteCheckpointDoc(); - } - - public abstract void beginReplicating(); - - public void stop() { - if(!running) { - return; - } - Log.v(TDDatabase.TAG, toString() + " STOPPING..."); - //batcher.flush(); - continuous = false; - if(asyncTaskCount == 0) { - stopped(); - } - } - - public void stopped() { - Log.v(TDDatabase.TAG, toString() + " STOPPED"); - running = false; - this.changesProcessed = this.changesTotal = 0; - - saveLastSequence(); - - //batcher = null; - db = null; - } - - public synchronized void asyncTaskStarted() { - ++asyncTaskCount; - } - - public synchronized void asyncTaskFinished(int numTasks) { - this.asyncTaskCount -= numTasks; - if(asyncTaskCount == 0) { - stopped(); - } - } - - public void addToInbox(TDRevision rev) { + } + } + + public int getChangesProcessed() { + return changesProcessed; + } + + public void setChangesProcessed(int processed) { + this.changesProcessed = processed; + setChanged(); + notifyObservers(); + } + + public int getChangesTotal() { + return changesTotal; + } + + public void setChangesTotal(int total) { + this.changesTotal = total; + setChanged(); + notifyObservers(); + } + + public String getSessionID() { + return sessionID; + } + + public void start() { + if (running) { + return; + } + this.sessionID = String.format("repl%03d", ++lastSessionID); + Log.v(TDDatabase.TAG, toString() + " STARTING ..."); + running = true; + lastSequence = null; + + fetchRemoteCheckpointDoc(); + } + + public abstract void beginReplicating(); + + public void stop() { + if (!running) { + return; + } + Log.v(TDDatabase.TAG, toString() + " STOPPING..."); + // batcher.flush(); + continuous = false; + if (asyncTaskCount == 0) { + stopped(); + } + } + + public void stopped() { + Log.v(TDDatabase.TAG, toString() + " STOPPED"); + running = false; + this.changesProcessed = this.changesTotal = 0; + + saveLastSequence(); + + // batcher = null; + db = null; + } + + public synchronized void asyncTaskStarted() { + ++asyncTaskCount; + } + + public synchronized void asyncTaskFinished(int numTasks) { + this.asyncTaskCount -= numTasks; + if (asyncTaskCount == 0) { + stopped(); + } + } + + public void addToInbox(TDRevision rev) { // if(batcher.count() == 0) { // active = true; // } // batcher.queueObject(rev); - //Log.v(TDDatabase.TAG, String.format("%s: Received #%d %s", toString(), rev.getSequence(), rev.toString())); - } - - public void processInbox(TDRevisionList inbox) { - - } - - public boolean logRevisions(ArrayList revs){ - this.db.beginTransaction(); - boolean success = true; - for(int i =0; i < revs.size() && success; i++){ - success = success && logRevision(revs.get(i)); - } - this.db.endTransaction(success); - return success; - } - - public boolean logRevision(TDRevision rev){ - return this.db.logRevision(this.remote, isPush(), rev); - } - - public List getPendingRevisions(){ - return this.db.getPendingRevisions(this.remote, isPush()); - } - - public void removeLogForRevision(TDRevision rev){ - this.db.removeLogForRevision(this.remote, isPush(), rev); - } - - public void sendAsyncRequest(String method, String relativePath, Object body, TDRemoteRequestCompletionBlock onCompletion) { - Log.v(TDDatabase.TAG, String.format("%s: %s .%s", toString(), method, relativePath)); - String urlStr = remote.toExternalForm() + relativePath; - try { - URL url = new URL(urlStr); - TDRemoteRequest request = new TDRemoteRequest(db.getHandler(), clientFactory, method, url, body, onCompletion); - request.start(); - } catch (MalformedURLException e) { - Log.e(TDDatabase.TAG, "Malformed URL for async request", e); - } - } - - /** CHECKPOINT STORAGE: **/ - - public void maybeCreateRemoteDB() { - // TDPusher overrides this to implement the .createTarget option - } - - /** - * This is the _local document ID stored on the remote server to keep track of state. - * Its ID is based on the local database ID (the private one, to make the result unguessable) - * and the remote database's URL. - */ - public String remoteCheckpointDocID() { - if(db == null) { - return null; - } - String input = db.privateUUID() + "\n" + remote.toExternalForm() + "\n" + (isPush() ? "1" : "0"); - return TDMisc.TDHexSHA1Digest(input.getBytes()); - } - - public void fetchRemoteCheckpointDoc() { - lastSequenceChanged = false; - final String localLastSequence = db.lastSequenceWithRemoteURL(remote, isPush()); - if(localLastSequence == null) { - maybeCreateRemoteDB(); - beginReplicating(); - return; - } - - asyncTaskStarted(); - sendAsyncRequest("GET", "/_local/" + remoteCheckpointDocID(), null, new TDRemoteRequestCompletionBlock() { - - @Override - public void onCompletion(Object result, Throwable e) { - if(e != null && e instanceof HttpResponseException && ((HttpResponseException)e).getStatusCode() != 404) { - error = e; - } else { - if(e instanceof HttpResponseException && ((HttpResponseException)e).getStatusCode() == 404) { - maybeCreateRemoteDB(); - } - Map response = (Map)result; - remoteCheckpoint = response; - String remoteLastSequence = null; - if(response != null) { - remoteLastSequence = (String)response.get("lastSequence"); - } - if(remoteLastSequence != null && remoteLastSequence.equals(localLastSequence)) { - lastSequence = localLastSequence; - Log.v(TDDatabase.TAG, this + ": Replicating from lastSequence=" + lastSequence); - } else { - Log.v(TDDatabase.TAG, this + ": lastSequence mismatch: I had " + localLastSequence + ", remote had " + remoteLastSequence); - } - beginReplicating(); - } - asyncTaskFinished(1); - } - - }); - } - - public void saveLastSequence() { - if(!lastSequenceChanged) { - return; - } - if (savingCheckpoint) { - // If a save is already in progress, don't do anything. (The completion block will trigger - // another save after the first one finishes.) - overdueForSave = true; - return; - } - - lastSequenceChanged = false; - overdueForSave = false; - - Log.v(TDDatabase.TAG, this + " checkpointing sequence=" + lastSequence); - final Map body = new HashMap(); - if(remoteCheckpoint != null) { - body.putAll(remoteCheckpoint); - } - body.put("lastSequence", lastSequence); - - String remoteCheckpointDocID = remoteCheckpointDocID(); - if(remoteCheckpointDocID == null) { - return; - } - savingCheckpoint = true; - sendAsyncRequest("PUT", "/_local/" + remoteCheckpointDocID, body, new TDRemoteRequestCompletionBlock() { - - @Override - public void onCompletion(Object result, Throwable e) { - savingCheckpoint = false; - if(e != null) { - Log.v(TDDatabase.TAG, this + ": Unable to save remote checkpoint", e); - // TODO: If error is 401 or 403, and this is a pull, remember that remote is read-only and don't attempt to read its checkpoint next time. - } else { - Map response = (Map)result; - body.put("_rev", response.get("rev")); - remoteCheckpoint = body; - } - if (overdueForSave) { - saveLastSequence(); - } - } - - }); - db.setLastSequence(lastSequence, remote, isPush()); - } + // Log.v(TDDatabase.TAG, String.format("%s: Received #%d %s", + // toString(), rev.getSequence(), rev.toString())); + } + + public void processInbox(TDRevisionList inbox) { + + } + + public boolean logRevisions(ArrayList revs) { + this.db.beginTransaction(); + boolean success = true; + for (int i = 0; i < revs.size() && success; i++) { + success = success && logRevision(revs.get(i)); + } + this.db.endTransaction(success); + return success; + } + + public boolean logRevision(TDRevision rev) { + return this.db.logRevision(this.remote, isPush(), rev); + } + + public List getPendingRevisions() { + return this.db.getPendingRevisions(this.remote, isPush()); + } + + public void removeLogForRevision(TDRevision rev) { + this.db.removeLogForRevision(this.remote, isPush(), rev); + } + + public void sendAsyncRequest(String method, String relativePath, + Object body, TDRemoteRequestCompletionBlock onCompletion) { + Log.v(TDDatabase.TAG, + String.format("%s: %s .%s", toString(), method, relativePath)); + String urlStr = remote.toExternalForm() + relativePath; + try { + URL url = new URL(urlStr); + TDRemoteRequest request = new TDRemoteRequest(db.getHandler(), + clientFactory, method, url, body, onCompletion); + request.start(); + } catch (MalformedURLException e) { + Log.e(TDDatabase.TAG, "Malformed URL for async request", e); + } + } + + /** CHECKPOINT STORAGE: **/ + + public void maybeCreateRemoteDB() { + // TDPusher overrides this to implement the .createTarget option + } + + /** + * This is the _local document ID stored on the remote server to keep track + * of state. Its ID is based on the local database ID (the private one, to + * make the result unguessable) and the remote database's URL. + */ + public String remoteCheckpointDocID() { + if (db == null) { + return null; + } + String input = db.privateUUID() + "\n" + remote.toExternalForm() + "\n" + + (isPush() ? "1" : "0"); + return TDMisc.TDHexSHA1Digest(input.getBytes()); + } + + public void fetchRemoteCheckpointDoc() { + lastSequenceChanged = false; + final String localLastSequence = db.lastSequenceWithRemoteURL(remote, + isPush()); + if (localLastSequence == null) { + maybeCreateRemoteDB(); + beginReplicating(); + return; + } + + asyncTaskStarted(); + sendAsyncRequest("GET", "/_local/" + remoteCheckpointDocID(), null, + new TDRemoteRequestCompletionBlock() { + + @Override + public void onCompletion(Object result, Throwable e) { + if (e != null + && e instanceof HttpResponseException + && ((HttpResponseException) e).getStatusCode() != 404) { + error = e; + } else { + if (e instanceof HttpResponseException + && ((HttpResponseException) e) + .getStatusCode() == 404) { + maybeCreateRemoteDB(); + } + Map response = (Map) result; + remoteCheckpoint = response; + String remoteLastSequence = null; + if (response != null) { + remoteLastSequence = (String) response + .get("lastSequence"); + } + if (remoteLastSequence != null + && remoteLastSequence + .equals(localLastSequence)) { + lastSequence = localLastSequence; + Log.v(TDDatabase.TAG, this + + ": Replicating from lastSequence=" + + lastSequence); + } else { + Log.v(TDDatabase.TAG, this + + ": lastSequence mismatch: I had " + + localLastSequence + ", remote had " + + remoteLastSequence); + } + beginReplicating(); + } + asyncTaskFinished(1); + } + + }); + } + + public void saveLastSequence() { + if (!lastSequenceChanged) { + return; + } + if (savingCheckpoint) { + // If a save is already in progress, don't do anything. (The + // completion block will trigger + // another save after the first one finishes.) + overdueForSave = true; + return; + } + + lastSequenceChanged = false; + overdueForSave = false; + + Log.v(TDDatabase.TAG, this + " checkpointing sequence=" + lastSequence); + final Map body = new HashMap(); + if (remoteCheckpoint != null) { + body.putAll(remoteCheckpoint); + } + body.put("lastSequence", lastSequence); + + String remoteCheckpointDocID = remoteCheckpointDocID(); + if (remoteCheckpointDocID == null) { + return; + } + savingCheckpoint = true; + sendAsyncRequest("PUT", "/_local/" + remoteCheckpointDocID, body, + new TDRemoteRequestCompletionBlock() { + + @Override + public void onCompletion(Object result, Throwable e) { + savingCheckpoint = false; + if (e != null) { + Log.v(TDDatabase.TAG, this + + ": Unable to save remote checkpoint", e); + // TODO: If error is 401 or 403, and this is a pull, + // remember that remote is read-only and don't + // attempt to read its checkpoint next time. + } else { + Map response = (Map) result; + body.put("_rev", response.get("rev")); + remoteCheckpoint = body; + } + if (overdueForSave) { + saveLastSequence(); + } + } + + }); + db.setLastSequence(lastSequence, remote, isPush()); + } } From f947ac3d5b3ab09727cd055ed6fc22c6f8e7b5ca Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Tue, 14 May 2013 11:41:40 +0530 Subject: [PATCH 06/11] Access_token based apis are working --- TouchDB-Android-Ektorp/.classpath | 4 +- .../libs/org.ektorp-1.2.2.jar | Bin 243506 -> 0 bytes .../libs/org.ektorp.android-1.2.2.jar | Bin 22865 -> 0 bytes TouchDB-Android/.classpath | 4 +- .../src/com/couchbase/touchdb/TDDatabase.java | 8 +- .../touchdb/replicator/TDPuller.java | 764 ++++++++++-------- .../touchdb/replicator/TDPusher.java | 525 ++++++------ .../touchdb/replicator/TDReplicator.java | 9 +- .../changetracker/TDChangeTracker.java | 565 +++++++------ .../couchbase/touchdb/router/TDRouter.java | 3 +- 10 files changed, 1010 insertions(+), 872 deletions(-) delete mode 100644 TouchDB-Android-Ektorp/libs/org.ektorp-1.2.2.jar delete mode 100644 TouchDB-Android-Ektorp/libs/org.ektorp.android-1.2.2.jar diff --git a/TouchDB-Android-Ektorp/.classpath b/TouchDB-Android-Ektorp/.classpath index 01dd0da..94bd840 100644 --- a/TouchDB-Android-Ektorp/.classpath +++ b/TouchDB-Android-Ektorp/.classpath @@ -4,9 +4,9 @@ - - + + diff --git a/TouchDB-Android-Ektorp/libs/org.ektorp-1.2.2.jar b/TouchDB-Android-Ektorp/libs/org.ektorp-1.2.2.jar deleted file mode 100644 index a3909eeffba76291bb68024509a2babf51dd7332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 243506 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

i<PBaLplR{V>6oL6s+sg)6JDuu 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

b zrL;oEG6#~W77;=ZVY&jE=#`p@0%v7XulKNjzu9p;4-oMTUndKe>YX*D#&8vo6ITam z1?qdnqh}4IdOiHd<(ossn7f6q{Ez9YKmQ+<#y_UBfUdnE3lp)pm5rl=qJy2GuH`>B zR8=g${#_KW4foB})r~|?XOD8+b&P7qYA5_C_`x}8o>E*k)*$_mR?Eu)$DgZp8L_-i z!F~9ofEQ?y5QKP<45&UJd0?-YkPk#=db6pJX`wt35< zJb`ocRv=W)bBsB;pr1}$q`8kJ7z#D%-9~*xp~c-@Vl`yQri}WeJNaP4=$SaU%nC3w zHmv@PMS&4dYDV~i@*Z}%n>eQ%S@bh78e$gizBgdARESDV}+`q6=sQ>_E^I73+Xpv zE-Jp>N=a-xq2dwR-p(TC@nx*gt>MCiMZ3<4A$!~DIV2d2{m9qw)?+}WZ%FYA%GDAc z2hKyjzk;rr3FT1Yln*bEp+d|RDYkO?tw!pktyH$O+qb)P+6Sep9c~1CDa3hUy8{`) zR-=V8$G=5+0I@_59;fSSLzpT-9I0f{$yy?sCo-4byTc4BI}R)s;8x9^D3L#;aus%c z3*|NTl)`FRvpgpE_?eNbjp8GBj!ER0T&3qrL$5@QlO#4rgh-T82@e7cBOzEVAK}rx zL>M7c2CjmC@Tw6gi`*d)uJsHVdVEdKOH$Zkk{S92Xqoo;9(M$EPR!p?uRvB_Pg+Gg zS+((|k6cLpS4mm|$>6 zAu3IpUx?0|27@x^7sE4{r?fG_^BQA}=Y_(nufCisnpP&zWAihY^7ZqEuN_9IM2jUD zOU~-Vsxd?~)A!cs2{w-l`za$i;_6{E7W|y=-mp;KxLDrbN1vFmp3|4QHU;3lu~(nO z?`{;AhGjLE+S!%tm%wkA`ekB>2>4!kmW)cI)P=*tR7$0USIdXg>BJ0vwL@z((wY>Y zI3SaA#S*oHV51P0_)FB`_ZMjGzbjdaZ4f)VT99p#q~F4zIohCS3An}&_QS*CBrNf# z8X8wMM|ZChw;o3T;$tRrT#|nbT__sM#RU>d8`I|;>{!~Bkxdag?-Mz>7Rrgd63QMk zg0DzecW}Ix9gQ0VUJw!XyOU!BXnDiK!gHf2 zB0>VaNCpF0Rh5{j-{=6Qwh2K|c|#}qB`oLIgwXhi(gipIe=gBV+bbCH2b_MRO~0Hl zjNuI#mZ2;kq0Ma51LM#R7W{6j3D;8wWPu%E``A^u*ppIIjC*WiPTKrh#bp$5Yzx(G+a_s z3g6_w%XOAu&MCDo4PanF2e z(clU&NhH1{NbY<%4CJH{Ghy@(7lI-`_GvnzEu!X}^>jG}jVQ zGZu|@op{yBG_B9TpCK>X@1-X^y4b&mPRYna_Y4$>Had!N!XTSF1?t3FEsc+Y+XBu zXo7&CYjSgNb*{o41d%1ykrmwqJmatOUH0WVAxtB}wg8dWL0eyozVb&-SIicp!nK1R zim8$xZ~R1RQmRZWW&< zsTNN%y+SCDjCCO?G5Ik}QK3(50KW{kk(!czYbrxU2GRo4BmQ*s$+=OBr}xIlUIjt8 zos6|PJ30SFJ%Ng?Z}*$S*bv-l4Y6zK>#QxetK4LoMXStm8dFJ_N?OpE3Bb_6J>HFG z@JIT~>~g4+{)mJzG z?>$%xyE3rO)eA#ET=iz&tc zEuBlb0r%5$^4zARJ=fygWrNg2*er^=;6K@NG+#(Xk_!12jSEetwv|?(jK3U=W2Nz+ zjlaxi#pHg`I+-8!T0z2o`u`%V{uUY}keF<}P}xVF5tPU{>v8Y0Hnfk$$-xlq+q!kjfLg4WIbZs|rP7(QBDN`*Un*TS*G2C|m*BFW^Ug3~- zD6PAt&AEuPrQI$|(<}DR*5!5Uns+ZT4=AyQ*m`K~6`%yMvbJx>wYVO%&v zLn;S9me3a8?XQGQG*n;Yt) zzzTJ(ai|^|4xT;V7q#7=(yavKC!WOQcTQN{-X{L`ZWIoSc4i%+x>z zK=ibfUlnY;=r(^wFEIFQ(F*@wZq*wU@U2>Z=Tjm*-WFAxadi@!clIaF`4lfuA^>Hu zZ0uKB8)|n~=x&B@I(qeEXV}A4VRD&UhTS|T6kS`6>s%A2KMCfhKtJCl31Mc3n!pI(@LD z9?)VRst4i5pz#iX2YvB6JO(=RSgOI$c6KTQwHv zFu|-y#rQN$Fp(B7efZ7dG}{H48iOi8z_RzdgE5KbsV%nYAI+PU zD`l6m&j8UE{|{PWS=s#X590lU&qn%ShMczH8Z7j5PHx7!~6F3uGNc4uQrXil+)0>5pW6b|Dgrp2gGhf*x$Y} z^Zn0d|KD2hcWrR4;qIX{U$6c8o6&wkid2`3jzAUzg)SgKRUm=fL;x5>5P0|s;sJ|v zm>3h1(UxJmpuS;dWu@rB_EZx^^0a`O2TKX1e4$}c)B4i#>nW?3t?*Q;4aiZ&#R7*?5LoK52{K7(~;q8baieBCxZJJuG;> zE2S=UDbWzq+;cb0A(zuDMVhtPR+TN}mkt-GN}&nEkv|JT5>4R9g?UBZk4o7NNsWSF z(?ffprDAqVBAQS+9aGVZ5@(V@z|S-dZ`RiEM-H)Xt5Z=Trs6Dxf}a z7fvij+cU?PhKVNSK)$vy0dJw6Heu_qBWGej!mH^A=4Yp#EL4h*Ra0{%jdQ-3XlQw9dS!jNI?OoU@9ZI}@F41dbRHHWt+rAZ`qa6R zrLh98Qogq$rNgy%AZTIYVj^kYa>*nQOFL?KC5`n@!-+d6YWby!_WYXfaf|CHwPhLo zlAF4TvB|a^IY%|ZVU!T~#2`odVjW0&E9W9>xrIv0!PCKs3QCx$5-+pKV34bk5T7yOT&w3#6Sn)l;N|q5_8K1U_E$eKrZ_c%q4JbQ$TDIu zBA1e4W;A>?jE%2jh{5Plq=bUd*n&jcEgEVo*ik$CHqwMD;WGUsk;zOAN4QwrX2kRu z@WI%R#62mbWE?Gdj0`a+&wvCok#%k)?8rww(acyEYj^L@kQEsj9T#H6(4e^k`qYyG ze0F3oN73BKy!UsO^FC}h-V$c0nt`@q;68{;w7o@H!kR$p^YK>qUJD|wx_cbYA(kOg zzrwN%8IDNS+q7CFd=mw3#)VCIc9a2%F!ggLx4gXSuC=ia776`bnX_3isn%ZBuIQ7^8Z8f}L+3d1mCMW|dvv?z+zKtOh3-QJv>#a6y767Tg>XZJsqIdiN$MRsLaJ0vJ&D z4?&_LMzk7r5Y7gVxuLQBZU!|-k_pA~xhkOr$QqJphV2S@9221Jf+Yz%=X=;4fZQ9I zs3YQx3IjG_{hJFQ0Oqc18T7~8(h zM~)C^^dleG+;`>3)o&s#rO?oXoC^WVECfE{0zwp7T`{q!5DVrU20T|zLTD<)mSv$; zgS41QQ#|;mh_IaCgCaK!#Mm)(K&~h9iP;LeK2H)ht1f?1a%rI&Br=+8PlJ8$gsK7WGXZD#~57fjW0s+CJp_7EH&zd zAA(1eF&xX4QxNXo9W_m7L8jZ^O$UdoC{_)mkd8db^7Is3+}ZFHSsA9HO@TdL>5mJ= zliq@j@q7t-fk>a{`gq*5)(iMX7CG`xvayFbKv}9EvUrqVxQ+^CXG`AIGIFgfq}H3d zqNZc#h;u-U;By8=j&Nc61>2jbh?IRZr-j_MW4CRr^LhOq6YoLiS%Y4H-46JK3>QPu z=Z6V71q98IW9xu|^@$K04uA6)23vRU{!W-(9e$c4(>S7l@D*r1%V6;0$dye#Qbwt~ z4-FSClPy=2S7USaS{7MOYi5)JR=DQ9<|zsx#e11}RGT3bmy}UWTsY`P77(g2 z*AXsz8*|=3i6>rMD!iCsmhVi?k~_qVINFC~(&;xEVd1pO7zfciiWjxAu|!t~AiLHa zB^GRp@#hnpPZLJw9INDgIwhz)h27Ul&{7Pt-2w6T2C)sETO94Xdqad)%-EGe z406+R23>nq<(TbSJ*77Lw%R5eYq6))RA%ZGF9osez#PAM|j~@ zf7iZyt$oqGd;RnBT}i-S{T<>ZdHO-h8{!>T{hj=U9&6$bdb*SA)D`DFw%iNY6NTF( zx!5e&)*0$C-^LoMN{&2SsiHTDLS7lss0FJXZ;Zr;Q6j%hVuW+3BU&q1(qIdKxfS(# ztxFwv{z9t}7#sq6+~e{Hl-akgbX+l9>>3*9q@ zC*Mm*F45>@pL*&~!hpx0@);`cn6dH1U`BfvU*Zly?dzYJMGB`gqDyC`_wschjXXik zFjghwX>B8e_kbX5bIz3EhgLjdo%Yrpph!7`iAS2y_&L7 zEv};(e_^-k37Xd6MXw0&26++U7Cdj|sRWhR3cbe7Bv91_km-g7+eznwwkrUh%`uR( zQC9_clk-!Sizdxy>xQBk@(FT>bzTWuzqvuUtr>&V3Ya1+CQJLO&5?dkuoZoqLwc;_ zBZjNPT#CRZ12MP$-t1$f=^bOQY!!B(Lr)5gRGwH2p%4%80nh zx=#^lBTp!OxaNp0%~{})qRUAkshb~9Bz-zjm2aE%j6*t)ee6+}Q>aBlfR1S!vG`F&wZ-jardtv`>G_KVr!(7I=D*N5MLskVo8FIzavjfr_(8 zblb_#ZgvMD&CT;kM@ml~d~+qLX$R&cJ^;)RA)KH;Czn~1QivgGk|Zz5JfP4Z^{qv* z*vBeAolPmZ(853tc6}^WJi{I_`te}o)swD{5mQzRHmw7JVIsfdMIf4LDF2kgZidyl&gi}oT9xY3|G#*`( z3^Si&^vC?l5A6}|{vWm&#QfU&==)fCbVD*6qf65D19%&{)GEKf$^bBV)>fK7iySLe z{$LfH>?SZKltif(`2vRdTD1z=3r1;$;|{7LQo(dZ*lf^;{6?uN*0?0RGV*dR0fqRL z&c_>w0G0H|t_v@Kgk=Yx_^;Ohaa%a0~tUQda4I5lBu0cn?4NIP zq&vTZTDqFkdR+D_&&eU2a4#LS5C{VBszLP6Eq(XeSX8csa^e?+f=>acMC`oRuY>BNzzE3K44t0B! zPm2~WQ-V?IJym(07d7$4(q2+N;}Hly`Pom?H;h3|;}kiKga$sWY>nPJSgg{B@F@u| zFwY})J6#{W$cf14~Bqey+&bTWralMLsiuNfwAQqJ%g!>x9J*8y815ou9o|6GAh zv3PmYYH%8J(t>AuvTl@Nxxj8%*})O5et5s?HJMRYK4YD1Wlz-0IhOR=mgLsBbeUN3 zTEZ8(x|+}0G>Pb9oMm}oU+%xF^SEm0tbo|5dC>Sus_jhGsd&a?biXb<`wpivPT`c^ z&tACc8Z$vX!LD`21GC5@Gu!L#DGKrt0~K*tyz&G|&5~K9vMUUQ#hXVNk$l`f zU~Q6h4fWIfEvb@GyFeDpJO6#a++pYmnfq58KRl*)9$7@=k!is0pC*z?O!1eRYgIR( z-Kt-2u=-jUg#uB-j$NR+o4UaB#@>hGNKN%X^1@I%?pRC#Kij^C z2H&;wxXp49BEZ#*&s;=uKmg8@{iIml#i=9zMu9~)nzAE19iV=}KiZ9cndksQ9c?0v z`uamd+fknJt3k{Wos&4CZ*t64c=7SIy~L_N;Ad+lb4_LSDagWYF)N=4frnT&n+s_Q zNxf~Z8Ur#ukm76ys3GZIM z4tF5EZaXiu?Q-_J*{wTgF^MWS*TYJKOE2bln;^|Q7rmrnKFW*@Nu#GNg0BZNB)=*k z5U2W!nIn!cHY)nE3mFxH%3-lTC)a|bMQm?jH>|4oohUMzzJo#d1?Jj zs*Pq+$b<#H(x#JJpjFFIy<0WNLZ4>`hvv^ESE@Gc*BDif@JjGLXmJj8fCj?){wTs}lRv7NfK#m`3T{CSpgk`7{y zv5HdSI;YqHwKbcUu&8V!e^OdNIzM>r&IoSG)-g#57p1eS%6UT(b(89)X`5iZV}Afh z3q6oUaGxZ4@RA@o2%r1)5tlZuBOm^3ZpNsS8}h6fmfBgAae#j$ajWbW{fu?as8eJ< z&O7aRfPE*=D4xaqDd3!LJxzd;`j~^nWd!+>W;n%|BtEHO^+Itnk)`R{V@?`aR~RN> zo|aaerrX&zapwT%JxGh8cTy=donFzrkv-y5amC$!f|u%a8ZYdFO%Cdl*ATYh1}~|N9`0R?*Vvqcp?42C}Ey)dsq`?CoL@N0u_s1F&wAu;3cp>zvvj=;O);6)-)$2bz!u5 zJCY^EDihbGy;?bCRndgiAMIP$SXU47at5#vXX0#){~u%T03}Jcr43h?+2t-*b=kIU z+qP|W+3vD!+sLwQ+vu|YdS~u@bMHHM&HT9{R>sPVjFl%&oPG8_``OPEoagq3sUa&D zFE6}=zw*X$UV~Z8r(3YHFuPo0*W8RuYBs%u)MDGX`5MSE5Jj<-K=O~Hx4sQurR-v> zk@}H*R%6DS3J9t?nnJBJ;zUi_33hpqMMdMhUZcXahG?>ywNvSW48A4zy*idz`vSkb zCE-OK%Zz8`#2sRJgiS-!>`{f}N$i5G;UW;X3EZrrN?=x#z@2qDL~==X*3gqAx-ZCP((vBK&kMlyQvqmS$ zAkJnl0@ArZ#hN*lAt$`Qm4%k z@$+zU)bDW@Xf#U0oX=w?OLyM$W^Fz%po#m9+n!g_Rp!u2jdQ{}UcE)WHExSVcy^h0 z7*;KA8*36}O^Tu|J%2Jyg2hiO?9WFe%1yU4PJTAcb`zNREKq)<&2V@=nInBKd(3#T zN1**Ad-ueG6Oi?Gf4vq~zG$AWwmIB)teEylA$*V6j8V*<;kmYdLF+uaj}bQO%z=wu zlvQr(FQYB8-pI5AqZuZjL>oE^!S5Uw+=b2=j|S@{6JrHv)3h=yA0$cl(rB_kv}3nYEE8`yFc(lS@p06F%@QsNcpMDG zve%)`k}mNdj0Akd2{);L$Ggi?U!69uO@+3L^C(MD#@QDMAQQWyd zRgxyYq!DDD(gR=8h|aLX3Iw}gFuv)i&+sqoUm3&@_*3zS!}SNovGoTo!|V=_4pr;l z+@m7}REE*_x!7UV$!S9}X!l~39YE_eNPeOX%MS-RknR(-!Z_5f!?g-s`c=ev;#(dN z3>P}k8)a$%yH{2Camg+U&?HFWpBdxqKmXAhhS_(I9XntcMs?uX4|34I_NluCtA&Uk z1ra~+EBM2w;Qq6eE7k~}Xaz+4fiIrh&i}9NKR+N{{?E_;>FfXV@c;ezKd$yaPN)3u zOKJ_{mKeiR8N<)lLQERNXAI%04dRxV!e>n3w*2C*$@s^gkf32*{wD77D;}-ZiVh!> zIy8|cl!R5N9;=KDN>X5C%G9iH=KbuDlh9%u>uMD1Psc4N?CLFNFVyXPj z;|gOt>tO3-lLUhgV26F!ksbKJe?lBk4To~U{9$ls*>_>bauA#JTgN{V5IJCvrC0yP zPIbhuLqmzbhh0Zl2hD(AjxTxuJe&!+OVb)b3$W>e^?YJDKNzzEySkQ0x-xM`9eX58 zl~uKLs}6oNZxI3Tz+Ej}e}h}Hc*+TeH*b*vgfM#v0YsU-qyR(AUIKu7Oq=Ozak%W+ zYje2lnd@pyp7I?LI9Br(bpSP{&4OKe@EY@au6}ngC-ZvY&adEhW-l24KV0_GH5lBZ z*%K>V2$PpQAeqTa9iR{QXwf1Ic*n$@zD9*J#Edp`lLH81;x5`L0SGa17wn_}_HZ%g zPi4VF%v`xUmY7-d*UoTT)7R2)pH(~LaG$w5nwVLO*V&j|l{?)`Z_UA1W=~bYrc7S4 zfJbJo@*N0FpOPJRINsT7Lpa`<>yBVFrZ*)(1g1~X&Jm_h?oKkMPvOoIrq1kjUGSEf zn=rtZ`Ar6J#r(z(D98LL*c^R*nNg?gFcc5n6~&TT&fDsU|h;(?R@P2aE))J* zj(SU_FkY`lKZP{F#T@Q#k?Nkra63Gmg?^Rb)4$XLVs&za=6kr zJ~uF4pO{*lo=Vz3J_i_|8ylY+8b>xcJTX1Q&^yF1It-(`)fybH&rCH9O*MsJl>MDb zIylZaKJLiQD4UUL8kkDjJI**dUZ0R^nrwhug|A`t^B9vMg6t*l?Rr!P5m{`<%i8}-oBUd^{DZ6%GB2Ml-BgrR#ke(YLi19 z!1zYrcxO`TtAWX-ipiz(_`3D@`o;MAS!yCH9i{8n#y|=2b<99%YoM|@UeOVx-Ws9q zPFerwq*b@pIqenI^pVc=k-_wl-ZUh&{x!88BBdT8wVrT%C7IsT)^rlrU~;oJ1IKtW z`lb=@(9)M(_mxfe6|w69s_P+N%jRO=!4fT5v%PhF6)>=-QzbKL_xX>G`V_sq>Ju$i zBK~E%i>GqSTTvj3e2#CE!^_s1w|q-aFYD2t^mf*7{N|ggWwftT@Nu(6`_!nb5IfIn z!BmpQB1h_OMreoZ{(<(d?$Y{p z|Cwh?RMAw#68-U^fhjJ6i3NiN+!d&2NJylQMA}sUtJY619Z05Q+314k3}+t-mypPv zXgRj?%Ea**%6&wzOu=U)pxMH3R6LQAR=n&1&Dz?X^))j~KbDCAzfi%o{PoUt@_DlG zzWUSq9<;~YE2AI3f=Et^z(0}YPET2-5aoM5l_2p*=WCsVamlD1ub&1I$>P41wn}&6 zLc2o~KERXVO(uA2>>2{&gA9-@bU6j}M(o#3a?KiaT&65A z{gH7HudL~49VE4PlJZz@mY5V$9In#Hk%WWS*~P{&X?a6mygxq}mX4vNL{~KGYUSrD z@vzXiK)lvzLyg)xN3TkwHXuCayx-hyK7Yxf9JJ?X?CQR(Q<}IDUaVxcXI5w^D5eqef0xDq%0J&Io)yR!WJ;Fhz8W^RBkIL3zmC^Kq6?O#3vzxP- zM$VN6?O=#<$57fxAY5>yjQYMB$oq8FR7)2}N)3`31TwrZGCOkU23Wj1O2w*_@%|Oq zMamiGg^UJzA?U3B$6_jrWWF@#&oqLb1%j;hay=JE)x7Yl#doWja(vntIEDhHjR=v_A8n4}XUggFj=OU^qvh2&S#TsuTG ztP&f?xn?KtfCViL{SfU#+T+B(DoQ3n8@06Q?b;W+I)-MhRdkr^cRV&cv=7& zm=_bK%r-KcjPLWhw+qEy_tuDBh))Nlr{#)?&`O2Z1_@6Oo6q`=r3nBYZyL4oE%Ug|Z1Udni8# z>Y=1a?MgRH4;i|x*m%ye9g{kElU{M@?`z_+g+9LJXWq6fKXeGLF$~^t5jy&1>CSI* zAK`;-;usP~&b2e~X1RjEDWvjQFvKEd_kVPT4rb>CeAhZ$V)a%@=Q)D7XF39XG+pqz zJe^Lc7rC>lU|cl!3XaR2Tt10wVVgs)j&YIAi1YbF{@v(aB199f15Xj&=ShWG^bxfz zMTFI{KD)!XgiF#=w*ddhd=T_-tXol-q>L)Miur0j{_8`0xg6uYCzVs{`#g9S7OrTHx z$L`X_y_@8Y@#Mkkv!cE;x!k(0+A$Sp!(sy-1e(Ki-ux7_)}iL9p(+GXh2i;;Q&bbi zmo;Z7b8w+_s%Cp{o73G2Suw^O)dCU_^4z8RU6RKqidU6&o6}$j!dx0#d!5x>!mh$> zfM0OEl-+~NwbDmg`I@FT)x19^A^y|_4hoCws}cwxxpn9hqmnU?G6QjvhSA?wQ^T~M zaVJvk9RtJO^fT~H|IbC|fvFG!|6t0ez#6chh30Seg~I$ImtIPn`A8aq1v z7paG}Y`aY#A3_FLq&u4*2{-CC32C7-DtUO|Z%C*D6sIA328d|dHqKjn6fYn=)B`Cp zD!>oUw1x4@^XEr5;A?;W&=7=}K(nz6t6O!p%LaU=;+t@Ug4qd{z?(b6A@~eqF4+~Y z&V)WPKsA>F_(-r86Q};A3T$yVkGZ_3q*hN< z0gdtr$JQ9SSrTK1E5*;6zS77A=RkdK$#g!moQj--Y+;yd^c?SAgiIKh>JC+htK-*A zzPrDXo#a4^#^rt8li9Bw|7-szih_T+CyxK6(&QXD@%du`7DW?P=3#i6FtUQOg#+TOs^fd%VR!XNB#=N8y+~`&k4;M1VP2}L zAo8b6IdTk0KzFDY_BS}MVvL|=qy_^LGtsL-={-1ajZOEzL5idbt&T9is)$4ozqCL8 z*9P+2*w{LK5sVm#m>XLe{p&+esq*TGyoCBurDHili8Z#Y6i6fkWN5@G0U4_o+8Z1j zDi*9~hXqM&X>LtBOj%FK?$;rfcKGhoy~QZC>J&VQlF`5@?jvFHf_7hAu%%t8Vo8O? z8DLW2`MB;l^*ni|`Tn@%`w656(-oZylR&>7T1w^{NsGp z@vO((t0Jc&7pDIMerVcXG|f&WE8DU!pPnmt2$c&FkifPTDZ_;dxOdaw`4Gu(LSTdzeD1JWBbNM`%3U~Nagt{uQ$+2ZS8_CV@*b>1b&e8>8KhNM zkyyA%A(_o=m2b3I-Lxo_rC^k-k9olFx4_L)?-`M1eYMU-g0QMdaomx@EahFmz+I&& z+HgZa9A-scjXK0M7CTiXcim2}Sw}MWVxE>QZaS`BX@>HU_$SePgKFztOedRsjyKok z>FxO-_s?&A7^m;4CSgDC{gE}f!I=%y+75osmo^VvRVlE9pQJH*3K}BcAx@$-Iv#

2ToMQgQ*qnLD$9|%Q2G3h+$BbvVJ_{A()*WeNX0B{xo?sx@SIgv ztcf}n=pwFepG!$e`aB}WCVh*!Gc|*|p+;@rBDV5%c+d1gczYyUUxEiaAkgX68V8$K zIMe;HBO0mjx$^LIHeS22q13N%&>NMmWK#+#M4MqR;ldX>{$QCSzc4&;=@HApMUzXR zMR--I7OJ|8?U$$0dgMKm9!eWiv#`>6Z*2JM{tkGBYOU@ii$r4+rtiz|`acbtSssCF z%-Uyp_hZ`veh=VMCELuX)q8MI_tHNeiK5Df^#gA*nV^Xd z_Q0UEH0RZsJ3TpVm8}}qJ%FOd+1$Tk%BjsqGJ2*0+x{8Tmdyw8T}qe|Nm?}D@8FDN z5~@t4$4m!Qw(|57u)I^+GknH8TikoL0k)9p$IG zGygV8;L}i4KJ4?#b`{n}Ua^NQ`*FhHN?3p zbJ320w2}*}k1)HR{9_=U?~Z1MUE7Dn!Yaa%bi5EV1ecEnhp4n&Dy%%MSSHpWlQy&u zH#XddD!SwRp+8u=+MkK@6^U+8T1HvqglQJi#l~~Fmt1FHs-X`dfp^Ia4936BDK9+s zl+dIA8PKF~fGz~tRU_l3P7K8EJoXTvtFZRlMWRO|0|J&%0N#{+*9i1x{d7Lk^hT;m z5l{vxM8Tz&;eOT1$v;$P43G~17@P)jW1o78A#^wJqWhKD2X>jR$-aySg*TaZHC`UL;jak@BIA^RrEUL|Vxn?)I% zc~Dy|c)Rc*kd>D7T2v;CmzvZ|aW?w8S`PJ1Jk(3zW0&Vt!c7ht^v)FR`v|e5!S0+m*S-hO zRC+w@Dly(Y!vU)npHo5iXzQ%={85|YA%91BiCe&>ZvN|19D=P!hC%>>lR-7VnJfNq z=An_KzdXV)2NgNpCLR2Y@xcxu@N)_u&$G7sFza|gTL+gHfXhqr(Kr9Kn-Ts~jUN{p z<`&`CrPCm&%^qg{Zj@UV?SA9mxziRz1&K64fq;0viaA*S|IN12#!hCo|6{t9{91EB z_z=-B4{G2FM5yVAXX}pxReM5=2f`oZGQjquCvb?&j5THs9hjwA(Uz~BdPeR#+@&1h zuX@@Z9SM2MQz{%4pOO$$gTz*?pfLCJXnQ^TC@OCB?eh2l?m;~dLL5NDA0K&Qh@{bi z*YAR3tw9(hNHU@dxk0rNLFkh}_gP08s~*r15i1waNT{XR`z~m=NC)Q{ol6?Gvh=;t zNuCkrBq;BYjd27!ph53|UC?xp?pdR?0dOI0T`ZP%8ChsKkYR#lpsgrznYBZAH<4a+ zU_mog?ch2!I(5e3iall`ry`ZGrsF0Ptjd_AEp{7HWsFxaRwXY*tKrewFTI4pO*jZE z$VY1tgU2W&Ydk*v>Y)+yJzDY1oFz+oBFtcn2-$ zxmakV+6Yo@Z6r0iY&6`+RmRGAKL`S6T3J_lYb@AcCffN-3xx)@5h|~|#==`*XalYo zD-_<_ZM|>omH#sYAHx;ZE5Z}D^mCFIwL}l*5tXaQ6}oGv$sdpJm~$F+zZdmfAVFTQ zIainc7VtU}|zm;)gcB-F!iE7MN?5BSmRxA2yF&9wJY0 zV2k5w?p+-*@7h#^U7E}Wih7V_?dX5wsBkfqC0HrL3wRrxTB?WA4t`uvT#A_}v zvB=z6oj0-#CDe@B9pZ>ev0CgKuTeB+-4J968oL)@Es$p|BMHj(7>&&ndL%O3k0?( zDJaV*FzZS5dd)-R^$P|?I_Im}_&TOwu+5wPDVv|8Ob{%H6DH$V*t|YLu^s_)3cPsI zF}nuHTc`?}CNQ`LNbuIT?V-wVzJfJidPkf?fP9cgOq?4&u5qttgZV~d~H=vYO<;zoGY>}ch`{twvrYerKl&eHq ztVAd(wlutTB(7L+>lt{z$fVp?=1JJ_NIqf@s*@`?Xd3w*h2@Kn&`z^=iyHxtznj<*r`?rv=ZHtmC=ZgZ#_G?{~@_)5gU+ba&8WKb)YRV$>qkNdG z)fsD=RfZGfkv&FGZNpLdgB#2in4tuO(HgDDjOwird*XkP;nDFRFv%l%(|wTbrCykj zQ?I2-yG&-VA8~FyAC6~#=>dYM^)SL=nlKyf%LZf(YJqR0sMf{~dYkje-N+3XqDD|8 zp|>Rk5qF>l_xJDr5OkQRvJ2KyS9>e$?gnHw56VPw1=U$&Hc|r+9q>33HmmF~7_bI= z9q0C|EIDCU;*IT1D@|HxiUBuu7fQ|_z4e(O8;?TLy3NwQ*_v6Lv$3j1V#=+mWpt6! zzaJw71*8~u=SP6{Z`^tC8-bz3RvIdTKbwor>l07D6qOr-Qw|> zU$Vkm?+y0QP%JBn3+06lyjnv=2d$}+?Vj+(4Kb^R=&-jWbHK>?fHup%2eGy)z8at*{1jtXI1X|DXxzc-uO$P)eH41Z+|T(+dDw zMM(S*refSrH;y?S6)JSVXfK0iL>{c(%VcMu1*{rF|}p+h(b z5O$108BkbC@!4r~UCd6!LK|?qDk;yHP%po92XAK2$}Y+P`%W=)%4ezaeFcIV;Q+AKN5dxdwQZLeilEM}yB8 zqHG3?#9D)jI`+|IC8)HJOF@`3^1pFwvq1&g*wHS@?&{yF)Z?w^`J&71sJ7|i+= z2GjiS5Bk5Ej|){l9kGouKF>EcFYfD-_i0)KnJU3GM-Sqe8ubztECsvun9Rqt&F7L< zZ7x|xnvy#WZE5*XpWj8Vv_Wf7d1C~FOLIEFvE=0Kiq$IF@Ra#i=;!-QWVkMo zGTOnMV{#@3ucd3Bla|-90N&7-s@E56T>%igVms~h=(}&d=p=iPJGpFKAt8Im0Cs+E z0^FGJfU6<8!tGjg8JNXA=q$PGfMdm;q~Ch>QvtVI}>sXY<<0=en(g*sw=>wv&Q*+eo1 zuBtW0gS;9Olp)I|6PR8X$!x|lkrl1BH5Jomff<}&^JPwACzG_EN4%t;S2Sp09oFM? z_wS7>;u{iH+^Q+GG z9964T(s`$VPLVFFDRgD1$|_mO zU!gGZ9j>ml0?i3YP=UM-gV=Bp$>ktrnsDD@Wtl%AbXL6Geu6_C(!4CJd^;XNH*|tz z>8*R5`3A0W4~uacsl2Zos`$azOSV#MVZq7!SJActGzN+-{;v$yqLy-&%f1X421gApRBjC4ga#J?JzJv(Wpov5M^UXc>QjfYFvs?%BbB=($Cr$mlgm?^=*wBVs9ChdbvX^gjT=Re zJbPw4bNOD+7~jhuD*Gf~GgU6eL4Wuvb=MaqmXPDj(-`zC0(w1?Ba$T-W>D`6WA#eg z{g^WzV=~xDevy{o>y{s@@DJCDB;l9pMq?goJ;T~=pa2nV=jZX37<#Hu!TG>jZ?mQZ zMuq+<51SBr@$ktAirvYQ2E7^j;joBryK!C#m@NC#43>SAS@~;T)?449s1EI?F`!$- z?3;e=C`_nPJxD|-4d2LjqphI$aB71nKm={h0RB7}Q3gP$tkGw=>6c5$QkFzKE4;JU z_3YC&?Tq`0wZ=ui{5pSw0e9H<)$T7kUVhGOczah`#Lc0(kXqcMiII!*ubTo%D`@XCtvX*cv%j zJTlu^#im|)Zo{v1(~q)qx&|{suusp>_cn;L*D$@l3tHU2VHGpTsMUdHVY}|OrQwJ< z&kS9IIc0Z`cj~hJY|~=L3tUApT3~dWwhyM>SMP{w*UA~NRH=DdEN*)Gm9kwWOg>VN z{)}9)k#*q^<7KZ&)IpsVT|`)|FBy{&ZL?8zM;4*R*%G>mHbZ^vj21%E1>GnbhgRDq z%KoXRl=k@qV(~Qon)lNBoxyQ~3gV3jJx_fS5zeT8Ad z)?@+s?+rfyTVFdh)ecmWcOZE;;*b~=H*(hzvQHnWU55F0_IVs^c$>a!acJV5bx(-LfRH$N~0m^ z77|JpiL=wd7dq`_8(TgW?MNi@*J&LB8+m6!AY=wL!ar_Ww{osJIOjWTERHVh1YKD}TfP;+zec7FO)Z9)6{4^%cSN)xUqAz(X^K4+mO36Fj-kfwUMbCHxl7&$|!H z2PL8>g`|p|(4BA#@Bec2iJCCyM{nNUmhu+S@frJSJGd=4xZkacn7}we50$&GG)}c8 z$Cu%cbq&Je^7gmMncfz0kZn)fr7x*0jYE{&GfX zbv)~(H|~IK(iZtv*}^idEoml#1jLcW&RP&41b1>*m)DFGO<7kZhi!LQf6D711%e*H zK|z(mdjo|*xNQ@}y8SXmFm+K)-?qvmP_H*N`AW~TZe=ijykt*x0oh?(l?@w82;&vi zr5q+;@N{GhD1%j($X!$5RTvA1smIX_Om#WbHAS z!8BR7)#CterT(OruwRzhY~JFyD#Ck}=j*y^^ah*1ZeI6Zn}_Q%b_6{bY-90+5PDqE z50@8pPn)#Jl~rPC25-`zD|16<<8Cz1Uu%!)5ThL0okDm)jYPzXD8zmTwl@bYf7h)b z63466U}&bAaKgsw4ukwk8vM6Z%7v*{kc=crM}o;)Y^_+Y-5EWHOQ(ZV8Vu0 zwnhN96xlPk6t&1bReO)cWT*b@4xlsmf+#u8EW=jEN(AAb4eXb<|?og z;&Ga%kLV27Y!|H0R-nsug-YFSHg~9G=4tdQytV!SlCNkq?0S~JEe&U@FD?i&2AYr- zyLaMgT(x7(gxUd+`{32)_ve}Po-(cp`Awv#Mzf%xQH!G@z&@rK?WD)Hk!ftxZfx_S z^k@DIW9?1P+&x;Kg?Q?h?l7^K`{aA6T{1PZA+ zQAI0l26ebXCWh48#u;w~*$oWS`;86Qt$Tb2>`&P7BpQ$e?LLOcb;L^0Cy3lY zqKFEF1N$NwY{#6a7`>7{?5I-v4Gb{u9R&rS`0f^_9W`7mth$Ce{lul&UnK`#D3##v=2p z4WBB^A`qvC$PEgkfFCjfF%?69kLe_D$LnPFw~W4ao+mvI{pZ-;U^Ld#>8NxctKFoSPPBn*L!Azy zJ!AZ5dpsV$FJ2UKFAXZ+!HB8wUP^=OGCa{6_M7L9Cog_49pr4<@P(zo3EHr!WMePk zK~-Zf>A@xM z2HtoeC<&n@W<#pNv?3D;v#y$kvVn>dCoQa~0j4=AETlJi=T15coQ3;_LJbWshSI{_ zprEEV0i}ACSz$`JLgcDa7+Ht<(uO$5^wNX5@|{vAf?Ny$(y5YYxLI3~$^sm7Ms0kl z9aZHJLvZeiiW}7yF3 zgQe1NE-uedCM2&%yFPyuAaprL%fFoq&@4Lg0rQLv@sK{j}*8G$zcRa z=TlhVYFt59!(?34W|uqEC4D?iu02Jmkb@kEMZvO=1)qLlPRxrjn!rg(E>Jdxl9 zs(w1?tdUt*fw{Se-*fM#!cqCPR?n>kt9kf$iDNFc06$dM!$X#(utz`dwb=p<$g)lz zg@Kz}xz>7uf%+uO_Q`sue%!Sq6=zclu3I7|N~C1Fx@D0ET-e7%PF>r{#?GUwlIfgO z=4HjfZ68ly0qOW%g^cF(<|sRphNyy6sWa@<0m9^5>uI0aj71wUHm&d1#-|mg6?vAe zF{#t*893vNgpPu*)ER5C#wxiSEMRYaho%o9n!3z0w`ME16%x)h=kzMM%Ow{RR!D6T z3j{C0X%d5u;mE9pt8D{{h>uc~;@({OY7(8!l7q4rpzjRbk?C=2qpg3sR-6Vy%@f2C zJK&xYci&2qbKkRHDe+rov6UfdT98 zzlGkNM#<>S2^1^6@-zMz0v++QVazFlm5#CnKYz9P9#)Dqq9l~Ei6c^Rtigl!k4d6v_yD0=lpjWttSV!zZ zhBl!u-ln-EcLSfIXrTyR61w8qRJa!HpvJeGMr5=^u>etoIE#=Aqra#s!C>qdjWj5_ zubB+9%l$5aUXo(iPVt+|K2Q@$MhTGz3Ad`xOT6@=vO)pc2+LO7ttPJ{5(n%`FZtx^ zSKsq*i>l=c=kBS)pb-C@ZCRujN|;&`DgPbZnvAjxBTSSLmh-i6k&+@j0=1BcT$M$k zil|L&w-=R#)~7^jC1vVI)9|0;c=M&rCIugo1$6q`;nNn)=(qE)i=~oUU3J=lYC}W~`hUZIqp?ms~c5nKgUeS9oC{cJJ+1`gPK0|D`keXe+ zJ5VQg z0jroiAA-Hs!!w@OEK3VrFkAkVZ0*~ZzE~eCV6tDl*K#g$x&eA-UkLxN6??_bk*M~j zVqiJajyIn132?|mlxPHZ2${KTeqgQ$c88z8z#)r3NU!BHnzdU3$%VePvre!>q5ZiG zZH%zWZqiQp5H?gtGCjsm=oFm6n1r9(Ba02JRHLt#~b`K3JPx!i5mh~#uFRFSD2&G@jS1Dt-Dp8;IUAjWTu6=eqA##DZ-*D}B zQh_Vgzk&rfclbUnE3#Ju!3yfy32>pvA5Sj!58-Tqi>83LUd%Tz(W~Ej-AxdlF8MH? zCNzf=v}=dR>J|m%4SPFp9Uezy-;b#YYLN!ft97xwfy|ruXbo8XlD$jDBO@YdO23TdtrXLn-L> zSI7`E{MJkh37K-|$Qp?rn6dE;(dXPMD@=^cA3W1KF@EF=e9^7EXbwAdw1$0>jDp>! zAOJ}!K4xgCG%69ekxHwj=%#Kpt-+PB!&o$BCDnCo;<>O1%0-7I857xV3gE<5`6_hF zoHxk^0qm|IVl0|uh#bM+o!DUDzIW#$=a(tA+<(9P0@+$*0c%S*fWjV;!H_Y8JATkF zlfKk$&n!)b8+-7(gZBVaph&9qKhr;>hNU)^ec~*Nj=yf{Majr56V~pMof-H4AoDl- zj>t`QBvh`JhuspZpBe(Mq=8Xxn=3XqjY3-U@YbG9?+WYbWYWJNWqp7mwxZEA#6V-f zy|z*a+kNvl53(=^d@1TH`*HP^vHf=}%M!NcHvgrx6s0I>izJWoK_k`5RivOMrI`yc{h|EfOeNU%;8q`gY5R zeME1wg+@`j=`~5bChVw4xeZ;QWRdZ2PTI2IccE$)aYl8Eg}cKfoAljx(bAT+A^@GD z#R}ZSva_a(=2?r0qUSt;FQ;fIP8+=uav{!pVHLA;c`m9p;KxTRVOzLmx(Z=3whBF+ zXaS!7b&7qk*c8Fs=y1QkmCjJreN=!s25|JuB5}Mwn{x0lVeZWB0UiL%Ne%+guI1*_@@nz<`Bn<6;@5+Uxw+kd;==?CCOdz8-W_?xxfa zFd1QVWZ1b+@-)J7f)Hhb(%tWWC@#O`Df+wZP%YI|YyDm+aC3TqP8W}r(XV~9Hs~#A z;GEqYLPq^?>7FLKxYNXBkk4rNgR&_)CM{-1O)_Aep>Q-U~oiwRufv{5n^n1c$hdRK++@ddIJ z>_uW_+&~0u!lpp-Qpgf)zCi*WId6zrrmxpFMc;F#k*KltB6r{pk9Q59V6&7N>Lt)V zic=e6J-fS%+aNoHiS|&@6u{5410U#EO1tty!;KIVX9BJX1JQ59uD!dAnuC~`2 zL|usl^-K7Z^aA(ag$;kHOJpr5j}?8Hx1F#3-{CqX6lG-uZLRI}9sZJP`!67)cO*Y_ zA02}5rx~*b%#U?Rv?@4{puC(M?K{xg;8o0o_$J)RzmSXu;gqbCrY293G5v;gBj>DlTMUo8hr&=NNm0|3G{jEhx8P56R(-|zI zlLEDJyDYBZECIB}>TR8Re;m|FJ*l)CZt1@i)C<)H=*O2k$7gr-d*r;m{L5yWDb@?7 zcl100cn|3lG@q9hTm+}s!xGY_p+Ojrvkl~0hXV~vk1 zAYK~o(5!}P108pWf|!i4v(`GMB&(9gUYJ18t3am&xRVefCD-hO{p!6jI#LY=CQn>p zo6|f(f?b_Hnw9N9ayaMMDE5P!M-x`R9xAB`1~J)RdRM!#Uf zk^^|gZpQx1O;m{~*Am96>?#9U-3dv_lW|fGP!EiE7i<3U4C@XdeU-~)rTV!2%q9?y zrWWqy-@zs@R)Mq;GB{n?vL;#br;4laWufc^6+F+!84nK0eLk?6t(kB=tNi@_s(Q4l zLz0!;##s>*Xjl-yq1@sJvn)g_1)KK^N-aeQ3TC+v+KP*SJpx+aeK${BVPFnYQHoXn zbU^!Vn_vgMpNl500FsNjLf}+5JHi((Sv)>e0s%yUy6*F5T{Z#4@FJM(nk=lvL@zm3}PJ^UWm1-Dc8lb6dhhY+U*R*CLj z%jM0-KPCJBvJ*;wunmU4Tp{zXMU#KG%=)*{^Of=cAA?6(TNO(gWrHuXlb^;q_l570G-1S0IV0VuuJyq&lpNu(7h^kE+4u666U=ohXs zYL}sE*q9&SDSxc?yQy5kgV*13*u14`Cpfs{7l$c(5-Tl&R8%Dt8O(yLv|m@pK}yhz z%7~L{RJ^n_l`b{bL)wnUpLHdQ%_3#wV{L{ev@*@LU`sb;RlX_q=o6Y{kxG+UuFUGT z#D?czO&3lZ_2+l@yAC_U_$!B5#H6?NZTfS!S`@xpZe|76fYO3w$;XMhsqgK4!%keT$*%^6ybD0jT5j&Ms>o-;jHP=N zc0803c+07X=yT>gqF$LeG2IX@p|1a zMV)Kcci2~2S})@vs;$>HX)Cu(shY^e{kU?f!HaTOsaS}wXJiz+7G)T$;vLx<{|Q`& z=#IHdK9LFHH~oYR+Le#c1EZy^n+k}~x(5fc?*L}!D5hVx`|%?nsPxrimlPB)|Iw4| ztaci?gjYu426D0A5fmmti(z!0hsm#H*LLeC%6dKtRZR@8pA;GHs;JO5qDdhNs@r$& z{K3SQwiul-4n#hYgkm0%nXJ6e+Q^#=U0zDhUTz0DuT6}M?KaS8plq*%H`}-?Xc2b2 zdbNS{j2OIp5%<6YP%`lGw7{)@hR0*hNzUKCk$hjDmn)w)0CjOjD&i& zq2!oZR$=it3^Nl^3Bx$3lp>;T~JAJYY`*hY)o}4kwC!_M5ES(@5D% zcVaPKQ?GHAWnlXv9%_=Jh^Mx|wMg$53Ot8z+0PwL24Q@QxJW!;1RpG%Zu-G&fq97D zQzLgwWL!@cC3ne+Zo+AZaTku|=4`JgE(-1gyjYmT#NQqOWj8 zS2%F_*)=uXp!_HEa**yW;&%3EeM@Qoq`h$1D$z$5IJ|?`c;j~}$_Y-ahI{ZIF)T2S zN(;4hLHNoONIrUyX9MsT!#_N^$Vi=S4}txHFrK_dM}$J-N$phNeO=-FtK{HflVV)< z>3K+F`rTs~U(X?kBKm-u7#92u@^LES>JIUIs16o%RiO9{MF^U_`IC; zUtNv!)$L6GcbBW^TR9v5y~n+yJHLP9|Em(_q6RWdhnT-U!7MRdEL2Vmr41i>h1q5C zJUT?t4G5k%SR}S(;QiS_=CXBzRuzbl38M_w*`HQcr6gk-ZyeeFhgO)=RPvh8W%vy( zh7Dw|gl2t0f||I1X_E#Gf%kYIVi3#DT<`SHz}53sF193l&Hiv-Xiv-B3a_oeMR;?m zIi)nQFs@`8{Z~%7+}F=AcmxTzAMSw+T+NAc?#_6A>3)g`=oUWLK>zAZ`m{#|t$lft zv~WN`ME~ba_=o$bYWbHa+eX4tARQMhJeaTmni*tF&VwJH`8Rp_Ko#5{)|Mf3!Fusw z2~ZY|SC6lrz94unYLYb-D&5YNAO9a?R{@sSjaKH8m5gdcu++n_dgDBd)$x8h z0X90smCUR#?f_k&y}@Ld&E%#AInP4Mf=K+!*#{z}%Bajd01QrWD+ z1bdfRcjQ)-TGc4;kUnU< zbwtY}OqL1_RoeLVOjma~%{n$lV{qj~-RCJ*>Jx{GYQhXztR$Phq;tj>u+85?3-dN% zS8U&Ug`6*@H7HUv*ZOC2#}0YAJ0U$}ls_Cy(=42Sd&eao&$KT^X@;!gAreD_TUM+T zj_I!m?HH){q14KeC}VJOhSjOQvg_e`@HD#7n)(beTJqMB!i@ekO=Zc6Uc(cLZ# zi_ml`#-!BA?J{bJ!tmQMTFpJ{Nq9*O72zd%_W4^UBLJw9A^pC|&M% zaH9u8fmyWL4tv6U%y46#yIe!I1jz^LiRpPHLvgws7b0_;cj_hSThniXfFu60qw<|_ zZgGYXIs(3HH?}@;?(E-eyW2h?j_03=O|*dO;?F=`@CI|tt}0VvU0gA{v&dDk%+~k~ ziS}M542j2IiFJ%#QD^$#ZDMM`1+Wdxs#FA#BD$e zO`r`&YWwXSLBjIs7w#@l^vVQi+bFKM`|8MdI%I@jK%nZY>BS8~Sdq1$=iGfnAN)0` z$E>pT@uOmzeJEq$;xWW>la!Umn~(N%tlyNd%9bRx&=rx#hrb#^bxIH~e^nEd2tOgP za6_bxD>pYp*;oxd8`9)e#4|E1_t2=o23=5Qw6Q{lSe=CQ06V(4A? zS52H|hFqO1Tzw~93#^C2-_Ii8kyUe{%eC- z@l*yl5Y7D}QG1R?O!%d6Vt5^zKKc(xX|&6bO+gwK`A(w+nIS$-&4!x(-Wwtw2n&Ts zGM;xx#;x~$%u_GF53IDgo^kJwv@vz~_&&ktK_XG-F|577O{Uqg>k}YAg@naWz+z>< zUMk&WG&bNEv4huQvdbX6{;_+sL?mcp9@4Qb^S(4(ai(4AJ$vw#e<98XD($UVzF0b3 z*@#lA@y5AsRrnrz`E29aOJU(w*La$e)68UvBPjTc+b|Ttd4E;1z;@E&UJ1Enlt-NP zlJM9CNB6p3Z|pnqPm;F7kUaJ?VrQ96^HGx+VL)Sx9e3pfq#h(jvv>p$z$(HaV zke!-~np#>$j$v}y&s9*nR3_oPRJr2xTRYy{bAaat4&@jh6(<-&cYT@qezGww+LSBuYcT#t2qeXLeJnAq>xe6qgI_)D2_VZfS z$E{E;70u;>Y#Bx9$7(BJZmU2{7pA-88;#%!@WR1=a0MDr{Mh^pUUj5Z$tR?5>=O8S zb)2voSUI!NsCJM&MxM|IS_&r$E{(}g{?P)c8PZPP-Jg;2bWU?Q4Vm>7W+Ht)94u0u z2wxHU(5c5}Gm8(!r3hwniHNb-@g#i+2%GG%7|20GiK}Yg>{WFN3|EiXVxH9sm9dYD z@1aXO(FS|`i%#=w<2|B^E0qU`>oIWs?ofoBgRzmhAzpJgoGr9ByfSG zb%C@Ift>60_fMA)fs{wcD(kHldPwhWb@G17E1TItu^c5AfgIZyxoYi=pUP1ZfnR=dYvB&HDNci|$;lsP)MbcYg zh`W5!RQ5YNh)8&_gMumsMtVkihIb&uaKtZ|UckkU!|pIJd;(<%>>KL@Enxr4BF1wC z*YiHZj02V91vFQ7;Qp(@wt}&V6>vlh2x_hj1WI%y`Il8Q@coyn2gP(*fbqizKNYRc zmP7EL>u@bZy!b|r7XBJKJjd;lrJ}e3k?Hir>eZOwtIiP0jPh(@a7QtpFClb;m-`n8 z*%5^=cLBrpcf3r+3f-2TwF$Zkn!Wdf0)xSToD_CylNk(GWnl5_kpyV_S zT8#trVQ?aHv4(w5$Y>i@IyeWxS=TCKaL85ZQ=^G*F3y`o~r1B<}l>;7A{eddzw*Nd)G zz@{S}9OnlH|DcXIv+zM@SHV9Bd^0Z-$X|PG7dv{Q4W?sSE;3})JRJHUhmsSeT zC1crXH6Fv3eQP?Z^#GZ2w&eVnHbZyY`hg~m5l=Yoh`zFA$FZgp4(z5cl3q0Murs=2 zdnQQ`Fs*TTEe0LN-y2O}{!%TCC@JR4eFlOp1BllQLE8i-ew?F$nn@%K$N*1p^xv{tpUz~f{p1ePmQZ;$34J}aG@ts(H3H#2h; zemcN_z>%cxuMQ#_;23V6LowTG4r`EZCBCq6#)-jnRbI8UwUSc6aSeBLH?Sx+IEtBc z5Ut~a7L9r280_LIeq9G@_pvEV5$ya4aYP^nH$)|;Nk@T-#*CD67%c$9#2ptyImK-HjFItVUOw|F&K4w3|WAXLRV9t&%ADsd+ zB~+>q;nP!5E11rA!ryz&0nuST2rRQOYHkd|?q(dHqwTldsU5NGXo>S-ZxUo z#2Ym(bZWTL{|LdUmPFke@m+11rZomhEtlFWlvncx&PHt^!n;+Q+H^Q{e{tTZzdBa^ zrnDBfn+fAFIELCQvO|;2&s6G1Pd8RS%}VbZGM>5%*&oB*5p1Sg>tHSOO#+{0XhYmw z?qEZ}@8(ixRIVXwt=}_>As@@_Olpf{VumO?2G8mn^KV=0%~24zy1mVg(nvsbAzPkr zR-3VxDE23QPtg$95^F<8-N;q2oj>gzeKZ_9EJaJprqhpPDOufSfUm=Hf^EQfgJ-YZ zpIPR^ClMk|`tfd1tg?L85Fx?&-og-!>_NInW3p91LyZP!_N4%r(Wt40OK_rnPzap*uGJDo5tgso<=uk;Z1aykO@B z!F`_gIttWT}Hm;dP-I}peqq^;NSi5fjkrw7@(aH11~Nwj^$LA1iA zD63X5vqxQoknSng=I8gP`MT@vbhW%FOT6OuKJ~8MZs5bc@`OGDrD26qT-?+?d=K9z zaErAJ2=XwJK7bff)lrP=_ITY&TH#doHmwgVVt3xdCvB;rc0mYVXlCgtN)Wb&EO^g2 zP%B5iwZOZdoI)CNQG_{%V~QJ&8G4CvG9@ z63LHEuQw6QU{3iv2u@kIM6}+t2V$^#81GHB>IdB-7-DDDCDuf_3fQcUD8(l7=QHeS zp=O$_gLpzN;&el26*k_YT4`_e(;C~d)_*!D0TkAe_>WinOf!Rhre>e91cEVp(%Z0h z%J_AwA%%KQu21g;?ukD>EKam1e~Y)oyNfY17~&Dg5TNv90q4_wEl2Q!NA8ELG~^V@ zy4w*y-~+v{Zf?Aa3Vq%K>MGC&GYzIvT!rJ|8$#4fJ5|05lBNXQD($tftRAlM>AnWk zf{gwBJ-x8mK6(MO>Pj$Vkof*Oe-MR$n>DPW68hlS>l3F?3Ta+N_+vwe{v^~eX36DP{sX(&3%z?mWMYpMCZ zX&IF2;?u_6=_6C$!#;JCXjE453{H)=AhDXX$bbFINr30|Fnt+BoLWHBp9Ac+e^HxM z%#B^;osAvbNhFQk|8-C#N@Z0A*avweOW29k&qk9-%$e0NO8}9k*2wRoRQNRU61Izc zVu_%e89p$TjfrBTBbmovcEeg*l47CmYHi@6-}NG=dE5wxjQ%w%`~FFm?fFAS+xX7- ze#Zmi22Q(aidi2CkHYKCdVd=!1}Uo04Vg$1b_k}=REaczc>JRpmMbsCY!G z(1952mo-ItFvHy&1U=pmS(~6ef-sv1)&ofsahm%v?#+7#@10~CGRgwg_~~$0iq$^^ zRtO$V&PZ27r^Y~UM8jpvU=(9OhJ5 zkIph%7Ru_q)N@2EV`PPvhJ z(Y1yqvvV#!8p0w~V_^5bI3^XIVJNhojEOv{eL0?|@=%noBN2lx+RH@r*wwJYui@v_a z>EVf}TO8OvEl0IEMX`{V$-aa&L(5MR$7Y5`LOstgYC;1WvkMZRSR9hpqLn0eBs&XqawCKJgMY%^xhjrd##jrHWhPtF%faSll2PwBF_0kye6i34zNJAU9PX*xxw)8 zrMPseJ0KQj5>~TapiSy_`Pl3%$WZNI2kvrl?5JbX>$nP_P;{>LY0Ds1W5oMG(goN| z5Yin}aKJpVupV?0Xtcf~)a_)4WkXV?y#cF#3s&D$H-Q&MR*@ozb3@d_vHrtS|4;X7b4v|c`(V)&%;Vn+<80qSV^x8g!VoUQP~(Y z43Y`Mw2B7_6&30b0zUmcs!2R?=gmcn-$G;caH8=O4aq(LEE1I~yAgg(fX?8rN8_FcyanuP0ZI%IkQv||3g_q7s6Pk1 zf4&vuYrp_;=4N^K+oenxzsAQ7ioA~h6k=o`2TGGhM1mFgDKLKng=|ogkv92G}LDOMsk3;F}~w5OJ@>=^p|#OxVs*J8fRwr4rGi9jaCS@37!HNuX%_}najBOEmF!`c3iZuH zF9s^vb3+5ELhpx5X2(=OsoSH)oHIfV?{QCE<5&phM$n4ZPJe{=Cp^xT#>^~!BrC%% z86+#yE?S#r?jrXMMx<{Si&xE6;{k-MHhLDhn6&-$-|uddj8N?j6SE9g**1cBt{iFx zqZk5Pzt+*%gV()9nKby?)8(c`Ty4hhINYinq3S}N+F+&r6*h1%BDZR-pu((S4pXW> zjRm}gr9U^W;BkX*dTIaqwCL=OaTK%2xlC#Crt4-7?2f4~Eb!?lo zp)PeU#BMAhwUSHz54C~S^2>})Om}-Qq_(+S1Cw9UYoo%(X5IQ<*8swzwSE>otyk{8TAqL!y;}3 z+2P^)@xCP!8mU-@a?V9zj(dT)3cG4eVzqWt@jk({x;FL%o`Ij@gCwyk>t%d_qRm+1Yw=)qg%-;Svm&5psmKi-tgL`{YnTe(ZrZwq%j zLiOn5yBlfGg(*89R|hx^38vU;Ncd2@h1Kr(`{_|v(B#|;uo0JPV7m^3S@#;5E%`P{ zf4ZHbwSJ8pE@C1dYFe&QV~M_XDZDsvm}rkYZa&CU81iVl$qrXN{t>qm!hGK*Fz`e` zK2Y)4s>{NM8IKHRO} zO#_%ZsQf6SmwbGj?|ecBF27{Ms5>`Ibmf{7y=cm*9_mxj<(_a}OEb%HaS!_R77o4) zPgDGiMbURn`Q}DCEZuncHv%v8rDeT>nuWY{v>KGNutBay56>z^PsWVMt@`=q$sW{g zR6o(hVVP;v<-CVajBUe({=_~-D!4@=3TGcGONuu|bjN72hO55c2phsZ|IR19Voyw9 z-c$@F{q-U^l>q92oK--%6DfapZk4(Wy{L*EF)-pbLQ4-}B@NK$uqRz=^6SqH<6&7rsV<+Z6-pK5k3+-Z2W7YJ#XUKi z+PZ{MER+#WQAj48U{{jKT-4Sl&hS{y9aKx6Los-wsfK@2T8o244lS|6Js}=dU~OP5 zFbB`?HfaQ?z6*-hdhPY=RLWTSP}O&pzl4I7^x z4@@Vyt;2(Yk=*Wb>b4iOs+;m|BT>mEjSvKa+nKgAMA;g|n$nk62GXRHkU^9%;0ZIiTY48NQ*t4DgDJT#R!>4q zgvni%Cv{?ib3UGn22+$ADfEjJ34`)Z+u6r1C%c-YpR4+xC!vXaa zYPKzL134!w?e|o^!6J~m2Vilm*bWtc^!D zJUIHa@OEJv5|1lBO3AcR$Vjr`NZAG3RE;OBs0+dy@-tH$Cp%PhPAv+)f5(Y$hga>J_Bq$NY?lDwGEz$*I1r~^P4G~Gm zfd~^J%`^7!=a>a&=0&q8-FMPXVKB0=U-2(5?z^_O{n5Bx# zyz6kgVt7&c_TU)aqmp0OmdlntlNDz?EpmUf|0~9e?<%)eg>7=yf~Ti|>lD~)5R92u z5b{rKv$s)Sxi61mP6{N}ryS`7Zl}X#WB!43r z-Hm?OeT)}$9s-3I`1O?pYWsrx&9L3X%l3CBUl)W>D!35eKE|zJ2bMg@oxC!>ItOuG z?SZ@=)U&o5ij208R6B5Gxz3@2LImk<(^q z?uB49mFHBDVRW~yt5(x%ZMT}-3s$_Kw!>~s{Gg6Q@)JqSwM}xb&+?7*+HQrp7c6X6 z&~$l=Cp+zT{A@uzAssx%6T(J3eD+*125z8z=Sor+>I_{_NW)wKiFDQeNha@fM6*3% zuI})%*ZoD|Cz^r}OHpQc_n;Gvv(JcUmb)a=XDXB}=a*}!Qed1h`Mu-NS~)}f8%&_e z2SM6m_&S8Lzwd*zVMnw1NAHy-B6@`KA)y}hsPjQl=rSJ2i$+jLG^Fs0Ml8m8r`2KO zc!w~1_%nMj4zR?iXYb>(e^T?G1FBOc(>VZhV=mFnTMde-+1UsAX@d?7(k_%-7>pOJ z4|@(B{Rec&o)rZ+b;`c-_(e2d=SDSY(HPn4X-`Jn1b8Opfv51RH$F;E&9-Pk${bCvw;q?rZ zAu*3$Zm&kc2}C=$KfZhef1F!(YBv@iw#?TE%8}KNH&*SaW;)oLA<{Sv<0^t^y2xHu zW3)9!xY88p>GH?d`j=+{-Q08`H79s;>WucuC})h_+L=A-E+(!?QWJIH&Cz8i{&-^! z#xBz|;~=N6C$7P9M)aFossmU|-hD$h3*}?o0eM*8d>}-1Lj@i9{A!E+hyMJh$Jnrz z_jV1F-r7k$uUrt0ky0B~QO;<)S2O_e{LUPgE_uM*civ*}8;O_ZGm<8LLiIjC>vZ+3O?|g6vY?DTlimONM^H zPZ)yVk^Au^LCi2$mX;fyvhoc(O>lN@zY?$f=mt*76umf6qLDIvWGY&q2^7o|t#UyQy1zTRsNj9l^IwHo?0;an*eC!Gn?SNhJFm zwVI|q?GHZ@1AS!(_COL+FmDQVT@V0UG7COyL8Mu3;=dM)Ka&h95%htaYyz2`&vpn= zBL!4LI6D{G$q8`+^Du}XO4~$uK(urAr5l=zvR)Lz{4iDyq>{X4+X#G=UPqF>pC_Gs z4EF)+9Mp_?u;}i5!V?R=j9>;lt-R^iT(996onY11UrB3NPKi^^?7Rkl@E#*vYY8^S z2I59@`i`SWv&0H4f2YQTUT3F%D!J~nX{ySA?f1m_@}P)hAEOHkwOLzxKr#l>kOHa0 z&{NMBWDN>2Avv$N_2C6Gzz8!@>b+iT3}jqLe12++=D!u zw>@{(y*%4C;sxtXA@{16UBGO;ki<2b#(1C`X76q%IG zqkIZ!Q%x#XAiL!iT~cpqsc%7g7v2zxMZlBQ6FcPT^7s|kQxl~2>%&{f(JWNy6$mM; zMxC%JtB*>O&3)OoirEx(u`q%|&gly1+yp^iV`b)g(6y+11rw$s;17fO*|NC!GE0rg z?)8W=9=sS6Aa-=&J_^+e)Tx0Z7{H9Yw4R_&>S3$`eh%h9N!1%BV# zXIBZ`9gu_CPRi^ib=CP@7&;Rfcf)-Wi7pKKhm4hfM>v)jd3@ppi=p3wKXI>08OpM~ z|AL?NmZMw6LKs}1jTI`OK3=+?y!2G4deMTQNzvtUga2bqtbq8V&;xjX<0g`_GT`Ni zw6Cvk_o|^f#c1svYu9bs;Ff+zYA7zb)vDN7pld3c#@;+{qCu@OLsq9%^m|7Q6gls7Z7~!p}|OQXj@Iq zo(e8m?AVvn);o^K%l33qeG$HgI>Yl&c~^_v4YNga=LdY6>xjlNIgVlUC}W3adN6d# zIYMmqNAtxaPQ0UgEd{qBkY*$}ByxO+!?z-i>4=#a)-uS0)M;doR2%B4AJm9Mi}77B z%MGMHBA`CqvB@5Bb^_vD(`6fC7uAfgrWjAK22en;($>Ij%qa{hL_7x-XqS+PUxh@> z-*J9ve{nod>G#d>B^PXlEP+hKC*2oWZqOEetX9S5WzG}-Pg^Q(j24NxU8ZKj3WFUZ*n&QZQE}dmK>s0U2z;4w`|`3I({v=P>-7t#@p(V z?ITzIJ$gGwj)}`!I`Z3DZ#?Bp$Oea43(O-#fq9RFbJ@!W7arje|M=E@L;)Z1H98s_ za|@SZZx`WkUHyI5f}5y-3G2&J{7G&VS!cZO91h;mD*~c6))HA7Ej+g>m1J*o``b}f z!_TWFG1w}$>pk96G)ZvkxQFZ7>_aL1c;}r_jn*MGRw=V6fus~y?uCnXiBFl5`A)G{ zkzU&ia}0AmfcEmm0$Fqq&6fF1_ziNt|Er#uKBkM38yK(GnZj8&6w#QxJtCKQED>4T z!dvc<5xN^Lmy)g0t?zU(JGx(mCtsjnIt+zeDEDBXdtvj$k)3rXZ>-NG@B}*QO~H7y zDcnehN21DJ#Q1U$zD&-aqzc98iQLbb-ldbuogQ$UJiIx69KLoO9T%Rq_pyM4*~MQ? z>$|xNrVF4tC#0m(9uidAv_ylhgGw=TgY`%{;HiK#@>Y>yDngAN)2Y*y5Yyea?_fVe z5-V)z7e(d1d&i%6lpr}em=AjRb#4dNyR{py5?XA%p&$49QpB;9;k6-OCA*HaQ(aR^ z2y~42rxXhm9impUP2z4-lprCr4?^Yb8s$wI)_fj~bs9XAw|hG{-VRahO=Pb#RuxwB zV_HV+RrxsDAXNCA{9yLTx~);(Hyh*amgvdKs(V zsnI0$ijHYu7|i2a(zd&bP^Zidb?S&M1x~9--}Z4LgfA4}xrAQf2dGFGYqm%$OhWIg zkMciYKJ;qRi?$+|M(`?5J;G5V`v&E2;Wvu?h=LtONf%D}s@vxhd4-fGxKg3-OY9A+aEfg40vG5yS*f=Ozncp3MNUp<<$JE1wk?0w zL@&4G$OrGf7g>}L(BYWg;Ye`k=%SQCY!VdRRT2zy6vUr&D7tFUc*m3^s?;fZf+X1M zDbO1wihJJlz=i|`H%XF;{VYO-$ccs18XjR6JfJ(G?t7Ig2r9`&D z`aaA}Ej3VAg)qkAg`o=LIR96}7=EGE|uaZokYa*GKYHIkK#A$HKLbw=* zU~-j$FcjnFB6*Y0s-e_kGxGkl)Krf*MkW==?uqT^%#`6EQcf*JuW5;q3Ki67l6;kJ z2VMi#Oc|GCTz zY^f;%TWU|%DAj|_FU2T=>y?V?0)=qv`sV!W7{uR_@*yxM-T6sY2L*kKXu;+2`j)#L ziNtgBj)~`H!zsO(X=ZZcAhP|si&ui%lobXP<8pV+ag6)BE02$ zd#JLJuU`o1VCmpm6NQV2&fpGc^Wj__zOsI85^(XMNkdI_EKyQ&5K)Z1Jag5@ z0Gfd<*kC2xASqE-5T7}I)-v-pE^XQNvyACC>}JQlEhqadaptHfid+G^aWr^G24ymL z@nZF2y)hniD=4@PW75HI;B|n7sY2c_go6?e&Nuq*V(oO5sY_OR1HVX~)Q^ zX5$Sk%2*>oJ;iIlZzB4GLD(ygOHzv|lk(#)Yf+`Os}LM%l&5B*ax7*`zPW$dtshxH z^HETDj7=|~g*~8~bJEmQmi&+($6kIMT)u$gEXOy+>DhNJpTWpjP$I|GeXgc->Ltu) z>(v|a{x!aAk!E5oRUS=4hTx8mmY|?hzPO2^l&1S?fk?0Dm)?!cS>atX_1qH?;*aL0 zdFkdjWElrWz=0uuFk1ZK_o~XEkjPa+$Zsu6RLUP;O`XHUnnZq3SFyDiF*k}SjldJ^ zhcE*(V0H+Io$zMTZDN+_n)A1E2^@2)_4iaF>cJ2JQk36iMKIX0Su7am zunhx}sY}y}!d~ax$E1tgb$|0Kn*eQ|Xzp`MpAS@~!H8J|oU2?pHAg0n-GblWwmE24nHPXva>=@; z&_qp#noNE*p}JB_vC%DpVU{CYHgm~8HPllBLJ3(a)KGIc*`{BSKnh;LW_>i}3B`i6 z!?>D>+Y>}O46yb{x%$Y`mT={0?U8t8x_qTmJ&_$xop1^kJc#k;6ctdr@8C!d^2URN zQ7}P8w=TdGNJL-%5RB$QrVDo)yp`V$593@4^NE}qTGS?d>Gy4{cN|)%$63sos}2Y* zJk<#1b=aGrJs`pbT3O3S`twF`vbL4Gc2nw$`1)Tamx=8yXc%vgp59itnG_*w$y&jQ2PSo z0p0q9`(~!tI@B)&`sP)gK@-Hx@>Z{gy@9p8|ApuSE7o;^;!2uZ*I?pfK(_xr@?Yc0 zJ#V31#z1fZym+U;ARo2>eE${%t=*s|U+tN!IRTH|(vgsJ$#MgvF{%qXU~UnF?TOM1kb+( zLO?1b*g^*mqz{ALWm~766}N7>7re1s|J=z};p=6g_sRF>N~!)_s)NtKZL-nG=u0qn z*5GZ0@%VS>nJfpZaaJqe?$4M~esh;`m+hOyRdJ3@G((p)xrO)r_k|7*varnV1(w*i zFw@u613a8=8hr4=_mA&iMe<_aHt?07--u-TOrkQ7%WaR0x)+QGbbl$^Ok@9Q?iX4* zmIhV{+x~tWUNciR%ffN4)%pe^`4O*}jfdx?@hkS%ob$%vRG8sbE)$QSR^QNm50Bzy zKIMV8HPO5qXea7RS$PlJN!GV*U{Hz@-uF(dtx)tOCbJ)$$Pw_@`@nN~acXJ%|E zWZN%Pk6P)QFF6fF9kx;sAB#`^I3N0`$7HxSc$7+2U*@DX)Y zGp6(-BN5A{SBqjL4NcK`kjCK41F-eBG^wabrjs>1%VdOJ4nYL?6$DhojfN%JQO#r3 zhj&!9Wz$nPWEoDj!zyZElaip$+u_cNpyhc>i_+DT$ApkQUnCgGl1MV;Mf1cYZy$K* zijcxtiR$Vs@-U4L%8O5vjj*;rR|~n^DNN9rXea7PAC0BhRIc>~cGPs9rm8!L?Ak|S z;tqu!`_Cy7$bmC%Yi@r2yG9NrpUbHfgyZS=(N?e>JLB_gSClv@&yU%{&W~@P38p7$ zLQoG-7up7wcjBw!je--VO{QpfzT&J+UB37sHxP4Sh|dek9|Jj`APF%pJH}RTtL9QI zxK~5WZkl?#n8%dNtd%VGI8 zacBIFxMW!ag#mjnr2*lIG*;&y7=f+P>o6xRrf(DM3ywJ|qSv<}n|$W=B#2pon4|;C zZlVK}b47PEL5o*}2n|kU+rfyMJgs$<;GTxYEaID|ha{4v`BvL!7?c#v38~h`7y%a- zu05pf{dmTyul%BCxCfO)zAmI2k4NJ?`jgnbr{ZK0MN!KS$yoZ*udO~}T|9Wy-~F|E ze8wJ+`oXN2K%b|k%6dTw&&3FRz{@l@TT@X`nTlhp_^f~iOD9PTZNXw`2_#%U8?)6l zx!IJ7{nPY#q?cIF9HNwzp-VkjjhU>$94&5zDdc&vg{v8yF3Vae4ff-R|KTR6Pf#AR z048ECMoU~*&&!+FynX^^JCAYIZDS8r)!wMzUv=l^KZM+lE2tiyZH+W5GD(Ji_n5si z6#FdF>CA2X#y))8oKH&>{B7bRlQ$d@zJ{a!kABYeSlsCe{Pqzzrz0&#v&qP*KB2P|wLoZ?k<&U8rwNCgT0aAdq%1 z*||dD+@LXJlj62S^)PwSk+z-j^k1-tchH+Y>19HOE)3taji&qH#Sn)#qy+K9Pr@38 zzoCaOn#In9t&3NOMz%&%AL_>XSo>lL9|y@2YqqnJqn6;7?weh){s}MJ=e~QfR@t}w z^Zo2h&QAQ~pNqV15#Af$j^U7cDcu%8VTSvFOR-5Vf{#JmzDWj)Z3J(Mf&%wESIHX~ ziro?>@smw_IkXC`N|{x(1!+f+(8qe^fH=5($W1ab8th&pukb$(1(hEX3aNY)BEMujbjk1EXa>@Z9qTs+^EOOX(0^$4nd6s{o5 znL?$F=yk$&@WggN`>6fK2z;Mjmoz6#qMk-2tf&+0xR5bNyXAW=b zfar}6C0Z91`3j-jVWw(OH$MZlyh-wws$b%g4t#Ry>!1iV#8-s-g^RCuiuDW+je3e6 zcz-_1Uaetf2?rSB^aIw(B>!oe{nL}vzvf#7?dRA8+|w>C`4}3SFO{+->y|1T!!CvC z$sp;Gaiz*w2zjLEtE@C#21jOMdY?Ae<%lQEuApuLyqMP(QAmx6>0HlOJjdC;kDQFJ z>-vH;hN(are~UYh4J#p*71Jz0$qS#!vtXdd315?@pUsTq zlpNFF4cpPCd3#fT6G6z1W{Wj+D)&vB&WrZjtyIpL7z)8bIV+?a z$+Kv;30F>yJN~Q>hnzke$1U&;7N##VM1T#NV(BA>on@cbE(1{9gjpABVpw$(#7ybb zledj2Q}^u#c0?2mGatE}TH4*N)ru~~Y&*X7ek40LscfG>it4D-c#sOg##zfuNiV#_ zN?Vj+OUi6KPoqgPAhE@IDHwLI zjwAU7SI^N?6a#e@*22;1SFVpUVJ`X}uFYYXAGtO@Y`UD*)|j3i&axOh(DmvaYu{m8 z-(mWZ(CR~~)2HVug7ssj=CqmtddyWGxqN!zG>CZ#h)nM1vU9N$(zJ{=rP8kugX3av zKZt(9DJRb36|#aumJQ#c-gV4-FPiE`hZPKigi!@9lsaiFZ<2_bs7ul-G@oC2s2(Nz zY3+$rR3_}l8wh9+A%ITL)4}o!z`~^l z&@ZX~zYun|*0gTcR^`gWHp}!cxj1zRvD(6ww6frpL-M$5#U{WY!q@x}`W6!Vpu{v> z=jM$@PHU4zqT^i{i>4UfDS5#EeUIBKbaQhJlgH3@85vB`)Qq<2mghJ50 zvp;B?BtMVsPM;XWOJscgg4uwGG%nyH!O74|UA~r5=^h;Nvi;lAheVz}JKQo%q)soS zNmycN<1&4U8Ahtkf?D;hOt8TkI%f?vy-y{#bc2y23L0-69L`eEO>9LWCs~VLH_zeL z-+7^@au2yjp9$wTHt4-lT)A!pe-_%m2;Z~h*g4qR89O+c8#^kx$c{?VNKlJQ%ZrsLT}V?)j&3V_ z8y#1?7^YLCr&pz~t!1qH#KoHA9F&rjqUA@20D2@S7}^V9RR~DG_DPt3{P+3KOOR9k=lh@Z|2rEr z$a6(6wDFBhfd{;S3+?&If2BOXH^8Sqvq|&Ih>Hj-D$z=d{2KxI{g=T2Sg86}dM>yV z`21{G_SYQGW>tSY^L&T<^UHINE#TX8jz0&4f7kKXqELUz2nY1?zhpEt)3-4-cKoj; z|GT!&C3F1@V5IM)Z)K}*^b^9bBmh=n|4PpYHoqX)8af!8{FMLKx&sl^|B^rR=lo_) zPR}FazZvk~bC3L-+uYjD>c8g3`lIN7h{)YmeA|1kIack$5wkbDVbL99x^ z^Tj#f`Qmx$;*XT)clUE{ekUh=L$l|g!2eXNegzuB6WLY+vTOqV=h^k#A1Tjo3-IOt z7nc4TNZiKG*-6pK!C2q=_n1Yd;Am3-rW;_t_#CR|kCf-<1N6DSU`lO zxs}m>V#0NmqW$yA{2nmcK97}uq&z<&K%)OA`0p8Emw8VI4RppL;3D}05xD31!QuZm zP!VHequ(Pc><_9O00_^PzCV+xOz>O8KcwKFEdD*{T2{<%47l;Um8W$jHKpXy<^88Q%MdGg@#*XHu zHh}a#6GZZCej#pSWbF2PRAvH|`sYb68bAiR!JjG5&s5+)qy8Q1nO6`m4<;J`9Akh& zKxqF=0PlrqBfmK%6}Q3JBVtDbMe{*zW-U=&^t~ zy0L>XAkP1?ulu!8qik9zkpTf&0B{NaM15}5T#4V|iWoZ?n*E;4=avac+ExDqkog&a z{JCYmO8o@)2YteBfG1JM{~~$MsF1T*?@<5(+W^7&x$=iHzeSa?brP|4wlVrIxX(;g zE{xZQ1cqxmV7UI7T0w>1;>rSgy#BKXguiY<{K``5Z<0z701x;99=!gEr7Vn^=qI!()!(B5wAtC(IQ~YEo_Tq0ZPZ^3V5R(xIyCj) z<0%56>z#iiKF?^E?j~CA0ij0^nJHf?n4C|3V8|8SC3P+bQWgTK>JCJ{JO?^dcq)C_n;`EMT<#Gv)aW z1Fw328A<-pR==hH_rUm{60Tzak(~wx0G2;tpZit4!QX)a2O7rKc24Fl#{AAsW(vmk z&c=>T;xXJ^8g2OYJFVC;pbAOLzH@lW#6{qBF1@pq!18_xHhj=lf@+5xUNKLIj^|3|<-Wa4)$ zeg-WKnhyg9TF)BbgwRi*_!0jb^xs`w0eF|tcLaFyKVMOn*ER|RfPS+8^c(u0N(}hF z&dvp@$|{TF_{fmRM`4B;ihxGG0YyPcc|YV4kFZee%Du+b3m3Rt6KAZhN{>;O5h~@V zSza(qO&A>&Z1Rx^F`5IZp^chMIzF;Av&U%8|J*0%JLlZ{;kVXZwpxqd-e;e^&)H|6 z^PO*nbBRfMjV3C;SY0egEe{NM;@o5K?gz0X@uTSJWlrg=-AP9#9ADG?Ax4i(q;K>b zvaN!PwAwrtixiD0DIBu=?sqT11ii5W^`sPCy`H6Jq{i`;!-stWk6YSHD>t-~!kE_O zrd6LdM^6@|nMG=hJ}i}ZMcJ@FGUMP8n_Z>8J83JQYMVD%AhF@nUnZ2FLBQRGtC2f1 zt&h>ru)_&ZVojbS6A3Fuw1hnhdp?LSS_N3~_}6IoskU?SxAO$ zkJ0cPRtp{G5tE^9D^@LhHD9mQ>nv+m{*sL}zTFU0-Mvz*3ctj0T@rMiI!;T zM&M`1>R%yA;7kPG2%GN2I@QfA9K<-KOuP4sZ1#8@(UIpS&D^>llSk&;`>)^bYp;SS zn((4^D5bHomE|hU1*dS#dbWI68IIH-Ql)cJ*2u`+GI0@1vkr}*1?7Cx2|@Pi3C}}G zP{se(|F{BHD#fY@D+s4Ulhsgw~~jpaTtRuu87UfN#Qm%(KZBD)TuM7e%16lHZ+TR%Cr9Fa3%R`Tk6 z_`+8x3r(QpT)lBWa`>(3shyshf%rHL!_kiL32Fh-2)B6LG{wRavi8T!M;4#q3Y}xK zksdN$$v#zu9x8#y#Zt|g`G-&thwhuI%2VadRu$o2?R=Fo4&yobN(I|IB6aDRD@&DV z-Yqn58YNZTrHiO}GNh6ZML3b{@v+Zg>=+IiDxL9zv5FEHmLr*M7jcfpviBQ5K)ZIr zLFlO^z8_1Eg1wYV6&W;%1j!)BN+6bdJH7CmU`V)ud3F+Ixyhca1Z&wk%Cx;{zd{em zevwvJV5(z@hKPWhn^$f`K&3q-4X9%S?@tojB4V@d?yEL!MvGh!xM5U_8oZs!k&dc~ zxW;piT4nn-igbPOhGM?L3%y@ER%*Q_zx$QNZ`J>nT-W!QNU{$^&e{}v zV%7E**zX84@VznN{v`2R^jrUR#)XhG2)+&T*&d^z23uFs$e}$i-n@N+rsy?-N`d;H zuFFmWIvbgpuMJM7iiJ~Hd^|Q|>?YzXJf(b4T|N}X7X>8g_d8QrVv-_{R10u=fK*&K z1qc#-o3GbAfE>{Ujw#tCh=^j5<_q@m`3=-@q7|fO%vmUq5cnv z(JVGa@si5vSV6Ff1@1R~g{Gb`0*x)5#$ru(V}XlWZ;GNwCk~>mUoT#;6WG4M(%J3j zNpi3zIw|uAJYhq>-N(q1Y5id7+Ojc42A}T?`}bG484!=znCIxyk;g0+nyQ+w)|m@$ zrs9@x^+e44s9%usYoL3h2kENLc?F9`k{qF>cM^CbFad8HbMnL)3z$xnyQpM>$MCIj z;q;|GWgyRiHE8nCT$$vteDa*C-qi_kj1bK1G`Y(>ndAUIIk$Of!p@F`fp${bzaL@qBQwf7rY$FvW|EDcW9>2M*?g->*LQC$i)EXs%bSEUqBx<|RFqTMzDVcm&Pej>$!a zzJj3JyhM>tb{qDg-)L^lZdqJG%H*Q?Z(YNJo~`1+mXD{ z!{zWpT&nDxB7I>~|4P{5SJ3GaPU#UInVjRm!jVp_L{1SJwO!?pFxo1k|ooP*9``+miu({o`ICU{uj&FyU3K%UYWW-b@S?^rl5o z4++nl%=4QY@MZPSl~Csg{OCoJiyMV8A9ceJX{nx@mrA@5l4#f1SoL&QSn+YR7NOc1F-eObfpZPBi6Z`*&|5|u_=bt^rXE2Ia}hr}*oXDwLoG!t zgq6rDnll58>jmPhmdCWZIiE9c1E@6K{W6-;S>Afs=u>-x;2fL1v*O za*pmA5wRA&fqe+cvC-;y8Bm9X7Vis_R*?kxS&SQlDF0iTz-5cJcZprSGQ6gvmgj;| zQ3AV?$c06#HQZfJ0xO%*bjb}M56qMu@H~_8pyZGOnZc5|m9msdo55rFm@W4viV*zxhX7Y2ej^no}^EE+sF>pCYu5KRdOx<_t93kESQn7XlZ0t6v7dd2&_W6^@iObedb)HwlyxT;Ni*sF%y zg0Hdv-v!a^V=<@GY4QSeO($Bw4>KRv*9u2r zvb&BSo!}hU&Bc>*>5*z(PLUdgGyYiz5?x7$A1Dq1lf3&rl$!X0_j0)z1$di7<#5aC zFFyxl_63u?b2p4)?y7Unow;-DTzEc#-m&mNL1*WgFzcS?-z0!2BECag(I-FNsx%boCC zdPTQoKgBZl3U-L+pyE^PKtp26{a;1)xQL*rWXaL>ac;!scw-bUkZHt7I_6I2(d&r+2bfnBBzMP2l+ZYN=vAh zpKx1rn9QqK5BgJ09@8qMChLnGyhKQ_%P#sZ^uY^Wkp`t6Ck-Mb0RVEd@2N|~rC&eb{*Qg?3nBuut0T67{^LfzKWkq~sAW9_Fe z9(CtgSST14qSM(m?he5+lpd??+jJ3OyVq5ESRxRazsNy!zUF7u4t$qcfjX+^Zxef4Hw|a)lhh_)$Q=F(XebV3$u-Zf}yo?V~~7Z}Fp}tjLc;N=r^6Z+$9E2M~E-L71Nev;=(*?G|Iq zqLbbkL!B~74Z<#huXOs&fwLQ7NHuzgJ|?r)->CrjtTTRc@5MF>pFoz1k$yGU zE~YfpsTkI*BwVB)ROXmZ}|NLH%R5G5CBxhtt7 knRk1Ro&2g+rn2;1Iw=VcAnhPYS&Pf}(HOAE?GrBl2eod-kN^Mx diff --git a/TouchDB-Android-Ektorp/libs/org.ektorp.android-1.2.2.jar b/TouchDB-Android-Ektorp/libs/org.ektorp.android-1.2.2.jar deleted file mode 100644 index 1ae7e48fd36f9a7df8a369e8a4a33347e593c950..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22865 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 k + + - - diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index 264ccf2..ca6beb0 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -2272,18 +2272,18 @@ public TDReplicator getActiveReplicator(URL remote, boolean push) { return null; } - public TDReplicator getReplicator(URL remote, boolean push, boolean continuous) { - TDReplicator replicator = getReplicator(remote, null, push, continuous); + public TDReplicator getReplicator(URL remote, boolean push, String access_token, boolean continuous) { + TDReplicator replicator = getReplicator(remote, null, push, access_token, continuous); return replicator; } - public TDReplicator getReplicator(URL remote, HttpClientFactory httpClientFactory, boolean push, boolean continuous) { + public TDReplicator getReplicator(URL remote, HttpClientFactory httpClientFactory, boolean push,String access_token, boolean continuous) { TDReplicator result = getActiveReplicator(remote, push); if(result != null) { return result; } - result = push ? new TDPusher(this, remote, continuous, httpClientFactory) : new TDPuller(this, remote, continuous, httpClientFactory); + result = push ? new TDPusher(this, remote, access_token, continuous, httpClientFactory) : new TDPuller(this, remote, access_token,continuous, httpClientFactory); if(activeReplicators == null) { activeReplicators = new ArrayList(); diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index a1378b4..ec03f7d 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -31,186 +31,203 @@ public class TDPuller extends TDReplicator implements TDChangeTrackerClient { - private static final int MAX_OPEN_HTTP_CONNECTIONS = 16; - - protected TDBatcher> downloadsToInsert; - protected List revsToPull; - protected long nextFakeSequence; - protected long maxInsertedFakeSequence; - protected TDChangeTracker changeTracker; - - protected int httpConnectionCount; - - public TDPuller(TDDatabase db, URL remote, boolean continuous) { - this(db, remote, continuous, null); - } - - public TDPuller(TDDatabase db, URL remote, boolean continuous, HttpClientFactory clientFactory) { - super(db, remote, continuous, clientFactory); - } - - @Override - public void beginReplicating() { - if(downloadsToInsert == null) { - downloadsToInsert = new TDBatcher>(db.getHandler(), 200, 1000, new TDBatchProcessor>() { - @Override - public void process(List> inbox) { - insertRevisions(inbox); - } - }); - } - nextFakeSequence = maxInsertedFakeSequence = 0; - Log.w(TDDatabase.TAG, this + " starting ChangeTracker with since=" + lastSequence); - changeTracker = new TDChangeTracker(remote, continuous ? TDChangeTrackerMode.LongPoll : TDChangeTrackerMode.OneShot, lastSequence, this); - if(filterName != null) { - changeTracker.setFilterName(filterName); - if(filterParams != null) { - changeTracker.setFilterParams(filterParams); - } - } - changeTracker.start(); - asyncTaskStarted(); - } - - @Override - public void stop() { - - if(!running) { - return; - } - - // Prevents NPE in the event, the app is closed before the replication - // could even start properly - if(changeTracker != null) { - changeTracker.setClient(null); // stop it from calling my changeTrackerStopped() - changeTracker.stop(); - changeTracker = null; - } - - synchronized(this) { - revsToPull = null; - } - - super.stop(); - - downloadsToInsert.flush(); - } - - @Override - public void stopped() { - - downloadsToInsert.flush(); - downloadsToInsert.close(); - - super.stopped(); - } - - // Got a _changes feed entry from the TDChangeTracker. - @Override - public void changeTrackerReceivedChange(Map change) { - String lastSequence = change.get("seq").toString(); - String docID = (String)change.get("id"); - if(docID == null) { - return; - } - if(!TDDatabase.isValidDocumentId(docID)) { - Log.w(TDDatabase.TAG, String.format("%s: Received invalid doc ID from _changes: %s", this, change)); - return; - } - boolean deleted = (change.containsKey("deleted") && ((Boolean)change.get("deleted")).equals(Boolean.TRUE)); - List> changes = (List>)change.get("changes"); - ArrayList revs = new ArrayList(); + private static final int MAX_OPEN_HTTP_CONNECTIONS = 16; + + protected TDBatcher> downloadsToInsert; + protected List revsToPull; + protected long nextFakeSequence; + protected long maxInsertedFakeSequence; + protected TDChangeTracker changeTracker; + + protected int httpConnectionCount; + + public TDPuller(TDDatabase db, URL remote, String access_token, + boolean continuous) { + this(db, remote, access_token, continuous, null); + } + + public TDPuller(TDDatabase db, URL remote, String access_token, + boolean continuous, HttpClientFactory clientFactory) { + super(db, remote, access_token, continuous, clientFactory); + } + + @Override + public void beginReplicating() { + if (downloadsToInsert == null) { + downloadsToInsert = new TDBatcher>(db.getHandler(), + 200, 1000, new TDBatchProcessor>() { + @Override + public void process(List> inbox) { + insertRevisions(inbox); + } + }); + } + nextFakeSequence = maxInsertedFakeSequence = 0; + Log.w(TDDatabase.TAG, this + " starting ChangeTracker with since=" + + lastSequence); + changeTracker = new TDChangeTracker(remote, + continuous ? TDChangeTrackerMode.LongPoll + : TDChangeTrackerMode.OneShot, lastSequence, this); + if (filterName != null) { + changeTracker.setFilterName(filterName); + if (filterParams != null) { + changeTracker.setFilterParams(filterParams); + } + } + changeTracker.start(); + asyncTaskStarted(); + } + + @Override + public void stop() { + + if (!running) { + return; + } + + // Prevents NPE in the event, the app is closed before the replication + // could even start properly + if (changeTracker != null) { + changeTracker.setClient(null); // stop it from calling my + // changeTrackerStopped() + changeTracker.stop(); + changeTracker = null; + } + + synchronized (this) { + revsToPull = null; + } + + super.stop(); + + downloadsToInsert.flush(); + } + + @Override + public void stopped() { + + downloadsToInsert.flush(); + downloadsToInsert.close(); + + super.stopped(); + } + + // Got a _changes feed entry from the TDChangeTracker. + @Override + public void changeTrackerReceivedChange(Map change) { + String lastSequence = change.get("seq").toString(); + String docID = (String) change.get("id"); + if (docID == null) { + return; + } + if (!TDDatabase.isValidDocumentId(docID)) { + Log.w(TDDatabase.TAG, String.format( + "%s: Received invalid doc ID from _changes: %s", this, + change)); + return; + } + boolean deleted = (change.containsKey("deleted") && ((Boolean) change + .get("deleted")).equals(Boolean.TRUE)); + List> changes = (List>) change + .get("changes"); + ArrayList revs = new ArrayList(); for (Map changeDict : changes) { - String revID = (String)changeDict.get("rev"); - if(revID == null) { - continue; - } - TDPulledRevision rev = new TDPulledRevision(docID, revID, deleted); - rev.setRemoteSequenceID(lastSequence); - rev.setSequence(++nextFakeSequence); - //addToInbox(rev); - revs .add(rev); - } - if(logRevisions(revs)){ - setChangesTotal(getChangesTotal() + changes.size()); - - // We set the sequence to ensure that changes tracker keeps - // moving forward. The docs pull eventually catches up. Filters - // are quite slow on CouchDB if you are pulling changes - // from the beginning, we want to retain as much progress we have - // made as possible - setLastSequence(lastSequence); - } - } - - @Override - public void changeTrackerStopped(TDChangeTracker tracker) { - Log.w(TDDatabase.TAG, this + ": ChangeTracker stopped"); - //FIXME tracker doesnt have error right now -// if(error == null && tracker.getError() != null) { -// error = tracker.getError(); -// } - changeTracker = null; -// if(batcher != null) { -// batcher.flush(); -// } - - asyncTaskFinished(1); - } - - @Override - public HttpClient getHttpClient() { - HttpClient httpClient = this.clientFactory.getHttpClient(); - - return httpClient; - } - - /** - * Process a bunch of remote revisions from the _changes feed at once - */ - @Override - public void processInbox(TDRevisionList inbox) { - // Ask the local database which of the revs are not known to it: - //Log.w(TDDatabase.TAG, String.format("%s: Looking up %s", this, inbox)); - // We have already updated the sequence - //String lastInboxSequence = ((TDPulledRevision)inbox.get(inbox.size()-1)).getRemoteSequenceID(); - int total = getChangesTotal() - inbox.size(); - - // Seems to use up a lot of memory + String revID = (String) changeDict.get("rev"); + if (revID == null) { + continue; + } + TDPulledRevision rev = new TDPulledRevision(docID, revID, deleted); + rev.setRemoteSequenceID(lastSequence); + rev.setSequence(++nextFakeSequence); + // addToInbox(rev); + revs.add(rev); + } + if (logRevisions(revs)) { + setChangesTotal(getChangesTotal() + changes.size()); + + // We set the sequence to ensure that changes tracker keeps + // moving forward. The docs pull eventually catches up. Filters + // are quite slow on CouchDB if you are pulling changes + // from the beginning, we want to retain as much progress we have + // made as possible + setLastSequence(lastSequence); + } + } + + @Override + public void changeTrackerStopped(TDChangeTracker tracker) { + Log.w(TDDatabase.TAG, this + ": ChangeTracker stopped"); + // FIXME tracker doesnt have error right now + // if(error == null && tracker.getError() != null) { + // error = tracker.getError(); + // } + changeTracker = null; + // if(batcher != null) { + // batcher.flush(); + // } + + asyncTaskFinished(1); + } + + @Override + public HttpClient getHttpClient() { + HttpClient httpClient = this.clientFactory.getHttpClient(); + + return httpClient; + } + + /** + * Process a bunch of remote revisions from the _changes feed at once + */ + @Override + public void processInbox(TDRevisionList inbox) { + // Ask the local database which of the revs are not known to it: + // Log.w(TDDatabase.TAG, String.format("%s: Looking up %s", this, + // inbox)); + // We have already updated the sequence + // String lastInboxSequence = + // ((TDPulledRevision)inbox.get(inbox.size()-1)).getRemoteSequenceID(); + int total = getChangesTotal() - inbox.size(); + + // Seems to use up a lot of memory // if(!db.findMissingRevisions(inbox)) { // Log.w(TDDatabase.TAG, // String.format("%s failed to look up local revs", this)); // inbox = null; // } - //introducing this to java version since inbox may now be null everywhere - int inboxCount = 0; - if(inbox != null) { - inboxCount = inbox.size(); - } -// if(getChangesTotal() != total + inboxCount) { -// setChangesTotal(total + inboxCount); -// } - - if(inboxCount == 0) { - // Nothing to do. Just bump the lastSequence. - Log.w(TDDatabase.TAG, String.format("%s no new remote revisions to fetch", this)); - // setLastSequence(lastInboxSequence); - return; - } - - Log.v(TDDatabase.TAG, this + " fetching " + inboxCount + " remote revisions..."); - //Log.v(TDDatabase.TAG, String.format("%s fetching remote revisions %s", this, inbox)); - - // Dump the revs into the queue of revs to pull from the remote db: - if(revsToPull == null) { - revsToPull = new ArrayList(200); - } - revsToPull.addAll(inbox); - - pullRemoteRevisions(); - - //TEST - //adding wait here to prevent revsToPull from getting too large + // introducing this to java version since inbox may now be null + // everywhere + int inboxCount = 0; + if (inbox != null) { + inboxCount = inbox.size(); + } + // if(getChangesTotal() != total + inboxCount) { + // setChangesTotal(total + inboxCount); + // } + + if (inboxCount == 0) { + // Nothing to do. Just bump the lastSequence. + Log.w(TDDatabase.TAG, + String.format("%s no new remote revisions to fetch", this)); + // setLastSequence(lastInboxSequence); + return; + } + + Log.v(TDDatabase.TAG, this + " fetching " + inboxCount + + " remote revisions..."); + // Log.v(TDDatabase.TAG, + // String.format("%s fetching remote revisions %s", this, inbox)); + + // Dump the revs into the queue of revs to pull from the remote db: + if (revsToPull == null) { + revsToPull = new ArrayList(200); + } + revsToPull.addAll(inbox); + + pullRemoteRevisions(); + + // TEST + // adding wait here to prevent revsToPull from getting too large // while(revsToPull != null && revsToPull.size() > 1000) { // pullRemoteRevisions(); // try { @@ -219,202 +236,243 @@ public void processInbox(TDRevisionList inbox) { // //wake up // } // } - } - - /** - * Start up some HTTP GETs, within our limit on the maximum simultaneous number - * - * Needs to be synchronized because multiple RemoteRequest theads call this upon completion - * to keep the process moving, need to synchronize check for size with removal - */ - public synchronized void pullRemoteRevisions() { - while(httpConnectionCount < MAX_OPEN_HTTP_CONNECTIONS && revsToPull != null && revsToPull.size() > 0) { - pullRemoteRevision(revsToPull.get(0)); - revsToPull.remove(0); - } - } - - /** - * Fetches the contents of a revision from the remote db, including its parent revision ID. - * The contents are stored into rev.properties. - */ - public void pullRemoteRevision(final TDRevision rev) { - asyncTaskStarted(); - ++httpConnectionCount; - - // Construct a query. We want the revision history, and the bodies of attachments that have - // been added since the latest revisions we have locally. - // See: http://wiki.apache.org/couchdb/HTTP_Document_API#Getting_Attachments_With_a_Document - StringBuilder path = new StringBuilder("/" + URLEncoder.encode(rev.getDocId()) + "?rev=" + URLEncoder.encode(rev.getRevId()) + "&revs=true&attachments=true"); - List knownRevs = knownCurrentRevIDs(rev); - if(knownRevs == null) { - //this means something is wrong, possibly the replicator has shut down - asyncTaskFinished(1); - --httpConnectionCount; - return; - } - if(knownRevs.size() > 0) { - path.append("&atts_since="); - path.append(joinQuotedEscaped(knownRevs)); - } - - //create a final version of this variable for the log statement inside - //FIXME find a way to avoid this - final String pathInside = path.toString(); - sendAsyncRequest("GET", pathInside, null, new TDRemoteRequestCompletionBlock() { - - @Override - public void onCompletion(Object result, Throwable e) { - // OK, now we've got the response revision: - if(result != null) { - Map properties = (Map)result; - List history = db.parseCouchDBRevisionHistory(properties); - if(history != null) { - rev.setProperties(properties); - // Add to batcher ... eventually it will be fed to -insertRevisions:. - List toInsert = new ArrayList(); - toInsert.add(rev); - toInsert.add(history); - downloadsToInsert.queueObject(toInsert); - asyncTaskStarted(); - } else { - Log.w(TDDatabase.TAG, this + ": Missing revision history in response from " + pathInside); - setChangesProcessed(getChangesProcessed() + 1); - } - } else { - if(e != null) { - error = e; - } - setChangesProcessed(getChangesProcessed() + 1); - } - - // Note that we've finished this task; then start another one if there - // are still revisions waiting to be pulled: - asyncTaskFinished(1); - --httpConnectionCount; - pullRemoteRevisions(); - } - }); - - } - - /** - * This will be called when _revsToInsert fills up: - */ - public void insertRevisions(List> revs) { - Log.i(TDDatabase.TAG, this + " inserting " + revs.size() + " revisions..."); - //Log.v(TDDatabase.TAG, String.format("%s inserting %s", this, revs)); - - /* Updating self.lastSequence is tricky. It needs to be the received sequence ID of the revision for which we've successfully received and inserted (or rejected) it and all previous received revisions. That way, next time we can start tracking remote changes from that sequence ID and know we haven't missed anything. */ - /* FIX: The current code below doesn't quite achieve that: it tracks the latest sequence ID we've successfully processed, but doesn't handle failures correctly across multiple calls to -insertRevisions. I think correct behavior will require keeping an NSMutableIndexSet to track the fake-sequences of all processed revisions; then we can find the first missing index in that set and not advance lastSequence past the revision with that fake-sequence. */ - Collections.sort(revs, new Comparator>() { - - public int compare(List list1, List list2) { - TDRevision reva = (TDRevision)list1.get(0); - TDRevision revb = (TDRevision)list2.get(0); - return TDMisc.TDSequenceCompare(reva.getSequence(), revb.getSequence()); - } - - }); - - boolean allGood = true; - //TDPulledRevision lastGoodRev = null; - - if(db == null) { - return; - } - db.beginTransaction(); - boolean success = false; - try { - for (List revAndHistory : revs) { - TDRevision rev = (TDRevision) revAndHistory.get(0); - List history = (List)revAndHistory.get(1); - // Insert the revision: - TDStatus status = db.forceInsert(rev, history, remote); - if(!status.isSuccessful()) { - if(status.getCode() == TDStatus.FORBIDDEN) { - Log.i(TDDatabase.TAG, this + ": Remote rev failed validation: " + rev); - } else { - Log.w(TDDatabase.TAG, this + " failed to write " + rev + ": status=" + status.getCode()); - error = new HttpResponseException(status.getCode(), null); - allGood = false; // stop advancing lastGoodRev - } - } - - // Remove the replicator_log entry for the remote,push,docid,rev - removeLogForRevision(rev); - - if(allGood) { - //lastGoodRev = rev; - } - } - - // Now update lastSequence from the latest consecutively inserted revision: + } + + /** + * Start up some HTTP GETs, within our limit on the maximum simultaneous + * number + * + * Needs to be synchronized because multiple RemoteRequest theads call this + * upon completion to keep the process moving, need to synchronize check for + * size with removal + */ + public synchronized void pullRemoteRevisions() { + while (httpConnectionCount < MAX_OPEN_HTTP_CONNECTIONS + && revsToPull != null && revsToPull.size() > 0) { + pullRemoteRevision(revsToPull.get(0)); + revsToPull.remove(0); + } + } + + /** + * Fetches the contents of a revision from the remote db, including its + * parent revision ID. The contents are stored into rev.properties. + */ + public void pullRemoteRevision(final TDRevision rev) { + asyncTaskStarted(); + ++httpConnectionCount; + + // Construct a query. We want the revision history, and the bodies of + // attachments that have + // been added since the latest revisions we have locally. + // See: + // http://wiki.apache.org/couchdb/HTTP_Document_API#Getting_Attachments_With_a_Document + StringBuilder path = new StringBuilder("/" + + URLEncoder.encode(rev.getDocId()) + "?rev=" + + URLEncoder.encode(rev.getRevId()) + + "&revs=true&attachments=true"); + List knownRevs = knownCurrentRevIDs(rev); + if (knownRevs == null) { + // this means something is wrong, possibly the replicator has shut + // down + asyncTaskFinished(1); + --httpConnectionCount; + return; + } + if (knownRevs.size() > 0) { + path.append("&atts_since="); + path.append(joinQuotedEscaped(knownRevs)); + } + + path.append("&access_token=").append(access_token); + + // create a final version of this variable for the log statement inside + // FIXME find a way to avoid this + final String pathInside = path.toString(); + sendAsyncRequest("GET", pathInside, null, + new TDRemoteRequestCompletionBlock() { + + @Override + public void onCompletion(Object result, Throwable e) { + // OK, now we've got the response revision: + if (result != null) { + Map properties = (Map) result; + List history = db + .parseCouchDBRevisionHistory(properties); + if (history != null) { + rev.setProperties(properties); + // Add to batcher ... eventually it will be fed + // to -insertRevisions:. + List toInsert = new ArrayList(); + toInsert.add(rev); + toInsert.add(history); + downloadsToInsert.queueObject(toInsert); + asyncTaskStarted(); + } else { + Log.w(TDDatabase.TAG, + this + + ": Missing revision history in response from " + + pathInside); + setChangesProcessed(getChangesProcessed() + 1); + } + } else { + if (e != null) { + error = e; + } + setChangesProcessed(getChangesProcessed() + 1); + } + + // Note that we've finished this task; then start + // another one if there + // are still revisions waiting to be pulled: + asyncTaskFinished(1); + --httpConnectionCount; + pullRemoteRevisions(); + } + }); + + } + + /** + * This will be called when _revsToInsert fills up: + */ + public void insertRevisions(List> revs) { + Log.i(TDDatabase.TAG, this + " inserting " + revs.size() + + " revisions..."); + // Log.v(TDDatabase.TAG, String.format("%s inserting %s", this, revs)); + + /* + * Updating self.lastSequence is tricky. It needs to be the received + * sequence ID of the revision for which we've successfully received and + * inserted (or rejected) it and all previous received revisions. That + * way, next time we can start tracking remote changes from that + * sequence ID and know we haven't missed anything. + */ + /* + * FIX: The current code below doesn't quite achieve that: it tracks the + * latest sequence ID we've successfully processed, but doesn't handle + * failures correctly across multiple calls to -insertRevisions. I think + * correct behavior will require keeping an NSMutableIndexSet to track + * the fake-sequences of all processed revisions; then we can find the + * first missing index in that set and not advance lastSequence past the + * revision with that fake-sequence. + */ + Collections.sort(revs, new Comparator>() { + + public int compare(List list1, List list2) { + TDRevision reva = (TDRevision) list1.get(0); + TDRevision revb = (TDRevision) list2.get(0); + return TDMisc.TDSequenceCompare(reva.getSequence(), + revb.getSequence()); + } + + }); + + boolean allGood = true; + // TDPulledRevision lastGoodRev = null; + + if (db == null) { + return; + } + db.beginTransaction(); + boolean success = false; + try { + for (List revAndHistory : revs) { + TDRevision rev = (TDRevision) revAndHistory.get(0); + List history = (List) revAndHistory.get(1); + // Insert the revision: + TDStatus status = db.forceInsert(rev, history, remote); + if (!status.isSuccessful()) { + if (status.getCode() == TDStatus.FORBIDDEN) { + Log.i(TDDatabase.TAG, this + + ": Remote rev failed validation: " + rev); + } else { + Log.w(TDDatabase.TAG, this + " failed to write " + rev + + ": status=" + status.getCode()); + error = new HttpResponseException(status.getCode(), + null); + allGood = false; // stop advancing lastGoodRev + } + } + + // Remove the replicator_log entry for the remote,push,docid,rev + removeLogForRevision(rev); + + if (allGood) { + // lastGoodRev = rev; + } + } + + // Now update lastSequence from the latest consecutively inserted + // revision: // long lastGoodFakeSequence = lastGoodRev.getSequence(); // if(lastGoodFakeSequence > maxInsertedFakeSequence) { // maxInsertedFakeSequence = lastGoodFakeSequence; // setLastSequence(lastGoodRev.getRemoteSequenceID()); // } - Log.w(TDDatabase.TAG, this + " finished inserting " + revs.size() + " revisions"); - success = true; - } catch(SQLException e) { - Log.w(TDDatabase.TAG, this + ": Exception inserting revisions", e); - } finally { - db.endTransaction(success); - asyncTaskFinished(revs.size()); - } - - setChangesProcessed(getChangesProcessed() + revs.size()); - } - - List knownCurrentRevIDs(TDRevision rev) { - if(db != null) { - return db.getAllRevisionsOfDocumentID(rev.getDocId(), true).getAllRevIds(); - } - return null; - } - - public String joinQuotedEscaped(List strings) { - if(strings.size() == 0) { - return "[]"; - } - byte[] json = null; - try { - json = TDServer.getObjectMapper().writeValueAsBytes(strings); - } catch (Exception e) { - Log.w(TDDatabase.TAG, "Unable to serialize json", e); - } - return URLEncoder.encode(new String(json)); - } + Log.w(TDDatabase.TAG, this + " finished inserting " + revs.size() + + " revisions"); + success = true; + } catch (SQLException e) { + Log.w(TDDatabase.TAG, this + ": Exception inserting revisions", e); + } finally { + db.endTransaction(success); + asyncTaskFinished(revs.size()); + } + + setChangesProcessed(getChangesProcessed() + revs.size()); + } + + List knownCurrentRevIDs(TDRevision rev) { + if (db != null) { + return db.getAllRevisionsOfDocumentID(rev.getDocId(), true) + .getAllRevIds(); + } + return null; + } + + public String joinQuotedEscaped(List strings) { + if (strings.size() == 0) { + return "[]"; + } + byte[] json = null; + try { + json = TDServer.getObjectMapper().writeValueAsBytes(strings); + } catch (Exception e) { + Log.w(TDDatabase.TAG, "Unable to serialize json", e); + } + return URLEncoder.encode(new String(json)); + } } /** - * A revision received from a remote server during a pull. Tracks the opaque remote sequence ID. + * A revision received from a remote server during a pull. Tracks the opaque + * remote sequence ID. */ class TDPulledRevision extends TDRevision { - public TDPulledRevision(TDBody body) { - super(body); - } + public TDPulledRevision(TDBody body) { + super(body); + } - public TDPulledRevision(String docId, String revId, boolean deleted) { - super(docId, revId, deleted); - } + public TDPulledRevision(String docId, String revId, boolean deleted) { + super(docId, revId, deleted); + } - public TDPulledRevision(Map properties) { - super(properties); - } + public TDPulledRevision(Map properties) { + super(properties); + } - protected String remoteSequenceID; + protected String remoteSequenceID; - public String getRemoteSequenceID() { - return remoteSequenceID; - } + public String getRemoteSequenceID() { + return remoteSequenceID; + } - public void setRemoteSequenceID(String remoteSequenceID) { - this.remoteSequenceID = remoteSequenceID; - } + public void setRemoteSequenceID(String remoteSequenceID) { + this.remoteSequenceID = remoteSequenceID; + } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java index 7a6468e..8a8d981 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java @@ -23,243 +23,292 @@ public class TDPusher extends TDReplicator implements Observer { - private boolean createTarget; - private boolean observing; - private TDFilterBlock filter; - - public TDPusher(TDDatabase db, URL remote, boolean continuous) { - this(db, remote, continuous, null); - } - - public TDPusher(TDDatabase db, URL remote, boolean continuous, HttpClientFactory clientFactory) { - super(db, remote, continuous, clientFactory); - createTarget = false; - observing = false; - } - - public void setCreateTarget(boolean createTarget) { - this.createTarget = createTarget; - } - - public void setFilter(TDFilterBlock filter) { - this.filter = filter; - } - - @Override - public boolean isPush() { - return true; - } - - @Override - public void maybeCreateRemoteDB() { - if(!createTarget) { - return; - } - Log.v(TDDatabase.TAG, "Remote db might not exist; creating it..."); - sendAsyncRequest("PUT", "", null, new TDRemoteRequestCompletionBlock() { - - @Override - public void onCompletion(Object result, Throwable e) { - if(e != null && e instanceof HttpResponseException && ((HttpResponseException)e).getStatusCode() != 412) { - Log.e(TDDatabase.TAG, "Failed to create remote db", e); - error = e; - stop(); - } else { - Log.v(TDDatabase.TAG, "Created remote db"); - createTarget = false; - beginReplicating(); - } - } - - }); - } - - @Override - public void beginReplicating() { - // If we're still waiting to create the remote db, do nothing now. (This method will be - // re-invoked after that request finishes; see maybeCreateRemoteDB() above.) - if(createTarget) { - return; - } - - if(filterName != null) { - filter = db.getFilterNamed(filterName); - } - if(filterName != null && filter == null) { - Log.w(TDDatabase.TAG, String.format("%s: No TDFilterBlock registered for filter '%s'; ignoring", this, filterName));; - } - - // Process existing changes since the last push: - long lastSequenceLong = 0; - if(lastSequence != null) { - lastSequenceLong = Long.parseLong(lastSequence); - } - TDRevisionList changes = db.changesSince(lastSequenceLong, null, filter); - if(changes.size() > 0) { - // Write these changes - //processInbox(changes); - if(logRevisions(changes)){ - long lastSeq = changes.get(changes.size()-1).getSequence(); - setLastSequence(String.format("%d", lastSeq)); - } - } - - // Now listen for future changes (in continuous mode): - if(continuous) { - observing = true; - db.addObserver(this); - asyncTaskStarted(); // prevents stopped() from being called when other tasks finish - } - } - - @Override - public void stop() { - stopObserving(); - super.stop(); - } - - private void stopObserving() { - if(observing) { - observing = false; - db.deleteObserver(this); - asyncTaskFinished(1); - } - } - - @Override - public void update(Observable observable, Object data) { - //make sure this came from where we expected - if(observable == db) { - Map change = (Map)data; - // Skip revisions that originally came from the database I'm syncing to: - URL source = (URL)change.get("source"); - if(source != null && source.equals(remote.toExternalForm())) { - return; - } - TDRevision rev = (TDRevision)change.get("rev"); - if(rev != null && ((filter == null) || filter.filter(rev))) { - //addToInbox(rev); - - // We add it to the log and we move the counter up - if(logRevision(rev)){ - setLastSequence(String.format("%d", rev.getSequence())); - } - } - } - - } - - @Override - public void processInbox(final TDRevisionList inbox) { - if(inbox.size() == 0){ - return; - } - - final long lastInboxSequence = inbox.get(inbox.size()-1).getSequence(); - // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants: - Map> diffs = new HashMap>(); - for (TDRevision rev : inbox) { - String docID = rev.getDocId(); - List revs = diffs.get(docID); - if(revs == null) { - revs = new ArrayList(); - diffs.put(docID, revs); - } - revs.add(rev.getRevId()); - } - - // Call _revs_diff on the target db: - asyncTaskStarted(); - sendAsyncRequest("POST", "/_revs_diff", diffs, new TDRemoteRequestCompletionBlock() { - - @Override - public void onCompletion(Object response, Throwable e) { - Map results = (Map)response; - if(e != null) { - error = e; - stop(); - } else if(results.size() != 0) { - // Go through the list of local changes again, selecting the ones the destination server - // said were missing and mapping them to a JSON dictionary in the form _bulk_docs wants: - List docsToSend = new ArrayList(); - for(TDRevision rev : inbox) { - Map properties = null; - Map resultDoc = (Map)results.get(rev.getDocId()); - if(resultDoc != null) { - List revs = (List)resultDoc.get("missing"); - if(revs != null && revs.contains(rev.getRevId())) { - //remote server needs this revision - // Get the revision's properties - if(rev.isDeleted()) { - properties = new HashMap(); - properties.put("_id", rev.getDocId()); - properties.put("_rev", rev.getRevId()); - properties.put("_deleted", true); - } else { - // OPT: Shouldn't include all attachment bodies, just ones that have changed - // OPT: Should send docs with many or big attachments as multipart/related - TDStatus status = db.loadRevisionBody(rev, EnumSet.of(TDDatabase.TDContentOptions.TDIncludeAttachments)); - if(!status.isSuccessful()) { - Log.w(TDDatabase.TAG, String.format("%s: Couldn't get local contents of %s", this, rev)); - } else { - properties = new HashMap(rev.getProperties()); - } - } - if(properties != null) { - // Add the _revisions list: - properties.put("_revisions", db.getRevisionHistoryDict(rev)); - //now add it to the docs to send - docsToSend.add(properties); - } - } - } - } - - // Post the revisions to the destination. "new_edits":false means that the server should - // use the given _rev IDs instead of making up new ones. - final int numDocsToSend = docsToSend.size(); - Map bulkDocsBody = new HashMap(); - bulkDocsBody.put("docs", docsToSend); - bulkDocsBody.put("new_edits", false); - bulkDocsBody.put("all_or_nothing", true); - Log.i(TDDatabase.TAG, String.format("%s: Sending %d revisions", this, numDocsToSend)); - Log.v(TDDatabase.TAG, String.format("%s: Sending %s", this, inbox)); - setChangesTotal(getChangesTotal() + numDocsToSend); - asyncTaskStarted(); - sendAsyncRequest("POST", "/_bulk_docs", bulkDocsBody, new TDRemoteRequestCompletionBlock() { - - @Override - public void onCompletion(Object result, Throwable e) { - if(e != null) { - error = e; - } else { - Log.v(TDDatabase.TAG, String.format("%s: Sent %s", this, inbox)); - //setLastSequence(String.format("%d", lastInboxSequence)); - db.beginTransaction(); - for(TDRevision rev : inbox) { - removeLogForRevision(rev); - } - db.endTransaction(true); - } - setChangesProcessed(getChangesProcessed() + numDocsToSend); - asyncTaskFinished(1); - } - }); - - } else { - // If none of the revisions are new to the remote, just bump the lastSequence: - //setLastSequence(String.format("%d", lastInboxSequence)); - // Remove entries from replicator_log - db.beginTransaction(); - for(TDRevision rev : inbox) { - removeLogForRevision(rev); - } - db.endTransaction(true); - } - asyncTaskFinished(1); - } - - }); - } + private boolean createTarget; + private boolean observing; + private TDFilterBlock filter; + + public TDPusher(TDDatabase db, URL remote, String access_token, + boolean continuous) { + this(db, remote, access_token, continuous, null); + } + + public TDPusher(TDDatabase db, URL remote, String access_token, + boolean continuous, HttpClientFactory clientFactory) { + super(db, remote, access_token, continuous, clientFactory); + createTarget = false; + observing = false; + } + + public void setCreateTarget(boolean createTarget) { + this.createTarget = createTarget; + } + + public void setFilter(TDFilterBlock filter) { + this.filter = filter; + } + + @Override + public boolean isPush() { + return true; + } + + @Override + public void maybeCreateRemoteDB() { + if (!createTarget) { + return; + } + Log.v(TDDatabase.TAG, "Remote db might not exist; creating it..."); + sendAsyncRequest("PUT", "", null, new TDRemoteRequestCompletionBlock() { + + @Override + public void onCompletion(Object result, Throwable e) { + if (e != null && e instanceof HttpResponseException + && ((HttpResponseException) e).getStatusCode() != 412) { + Log.e(TDDatabase.TAG, "Failed to create remote db", e); + error = e; + stop(); + } else { + Log.v(TDDatabase.TAG, "Created remote db"); + createTarget = false; + beginReplicating(); + } + } + + }); + } + + @Override + public void beginReplicating() { + // If we're still waiting to create the remote db, do nothing now. (This + // method will be + // re-invoked after that request finishes; see maybeCreateRemoteDB() + // above.) + if (createTarget) { + return; + } + + if (filterName != null) { + filter = db.getFilterNamed(filterName); + } + if (filterName != null && filter == null) { + Log.w(TDDatabase.TAG, + String.format( + "%s: No TDFilterBlock registered for filter '%s'; ignoring", + this, filterName)); + ; + } + + // Process existing changes since the last push: + long lastSequenceLong = 0; + if (lastSequence != null) { + lastSequenceLong = Long.parseLong(lastSequence); + } + TDRevisionList changes = db + .changesSince(lastSequenceLong, null, filter); + if (changes.size() > 0) { + // Write these changes + // processInbox(changes); + if (logRevisions(changes)) { + long lastSeq = changes.get(changes.size() - 1).getSequence(); + setLastSequence(String.format("%d", lastSeq)); + } + } + + // Now listen for future changes (in continuous mode): + if (continuous) { + observing = true; + db.addObserver(this); + asyncTaskStarted(); // prevents stopped() from being called when + // other tasks finish + } + } + + @Override + public void stop() { + stopObserving(); + super.stop(); + } + + private void stopObserving() { + if (observing) { + observing = false; + db.deleteObserver(this); + asyncTaskFinished(1); + } + } + + @Override + public void update(Observable observable, Object data) { + // make sure this came from where we expected + if (observable == db) { + Map change = (Map) data; + // Skip revisions that originally came from the database I'm syncing + // to: + URL source = (URL) change.get("source"); + if (source != null && source.equals(remote.toExternalForm())) { + return; + } + TDRevision rev = (TDRevision) change.get("rev"); + if (rev != null && ((filter == null) || filter.filter(rev))) { + // addToInbox(rev); + + // We add it to the log and we move the counter up + if (logRevision(rev)) { + setLastSequence(String.format("%d", rev.getSequence())); + } + } + } + + } + + @Override + public void processInbox(final TDRevisionList inbox) { + if (inbox.size() == 0) { + return; + } + + final long lastInboxSequence = inbox.get(inbox.size() - 1) + .getSequence(); + // Generate a set of doc/rev IDs in the JSON format that _revs_diff + // wants: + Map> diffs = new HashMap>(); + for (TDRevision rev : inbox) { + String docID = rev.getDocId(); + List revs = diffs.get(docID); + if (revs == null) { + revs = new ArrayList(); + diffs.put(docID, revs); + } + revs.add(rev.getRevId()); + } + + // Call _revs_diff on the target db: + asyncTaskStarted(); + sendAsyncRequest("POST", "/_revs_diff?access_token=" + access_token, diffs, + new TDRemoteRequestCompletionBlock() { + + @Override + public void onCompletion(Object response, Throwable e) { + Map results = (Map) response; + if (e != null) { + error = e; + stop(); + } else if (results.size() != 0) { + // Go through the list of local changes again, + // selecting the ones the destination server + // said were missing and mapping them to a JSON + // dictionary in the form _bulk_docs wants: + List docsToSend = new ArrayList(); + for (TDRevision rev : inbox) { + Map properties = null; + Map resultDoc = (Map) results + .get(rev.getDocId()); + if (resultDoc != null) { + List revs = (List) resultDoc + .get("missing"); + if (revs != null + && revs.contains(rev.getRevId())) { + // remote server needs this revision + // Get the revision's properties + if (rev.isDeleted()) { + properties = new HashMap(); + properties.put("_id", + rev.getDocId()); + properties.put("_rev", + rev.getRevId()); + properties.put("_deleted", true); + } else { + // OPT: Shouldn't include all + // attachment bodies, just ones that + // have changed + // OPT: Should send docs with many + // or big attachments as + // multipart/related + TDStatus status = db + .loadRevisionBody( + rev, + EnumSet.of(TDDatabase.TDContentOptions.TDIncludeAttachments)); + if (!status.isSuccessful()) { + Log.w(TDDatabase.TAG, + String.format( + "%s: Couldn't get local contents of %s", + this, rev)); + } else { + properties = new HashMap( + rev.getProperties()); + } + } + if (properties != null) { + // Add the _revisions list: + properties.put( + "_revisions", + db.getRevisionHistoryDict(rev)); + // now add it to the docs to send + docsToSend.add(properties); + } + } + } + } + + // Post the revisions to the destination. + // "new_edits":false means that the server should + // use the given _rev IDs instead of making up new + // ones. + final int numDocsToSend = docsToSend.size(); + Map bulkDocsBody = new HashMap(); + bulkDocsBody.put("docs", docsToSend); + bulkDocsBody.put("new_edits", false); + bulkDocsBody.put("all_or_nothing", true); + Log.i(TDDatabase.TAG, String.format( + "%s: Sending %d revisions", this, + numDocsToSend)); + Log.v(TDDatabase.TAG, String.format( + "%s: Sending %s", this, inbox)); + setChangesTotal(getChangesTotal() + numDocsToSend); + asyncTaskStarted(); + sendAsyncRequest("POST", "/_bulk_docs?access_token=" + access_token, + bulkDocsBody, + new TDRemoteRequestCompletionBlock() { + + @Override + public void onCompletion(Object result, + Throwable e) { + if (e != null) { + error = e; + } else { + Log.v(TDDatabase.TAG, String + .format("%s: Sent %s", + this, inbox)); + // setLastSequence(String.format("%d", + // lastInboxSequence)); + db.beginTransaction(); + for (TDRevision rev : inbox) { + removeLogForRevision(rev); + } + db.endTransaction(true); + } + setChangesProcessed(getChangesProcessed() + + numDocsToSend); + asyncTaskFinished(1); + } + }); + + } else { + // If none of the revisions are new to the remote, + // just bump the lastSequence: + // setLastSequence(String.format("%d", + // lastInboxSequence)); + // Remove entries from replicator_log + db.beginTransaction(); + for (TDRevision rev : inbox) { + removeLogForRevision(rev); + } + db.endTransaction(true); + } + asyncTaskFinished(1); + } + + }); + } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index de71b11..d68b46b 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -51,6 +51,8 @@ public abstract class TDReplicator extends Observable { protected static final int PROCESSOR_DELAY = 500; protected static final int INBOX_CAPACITY = 100; + + protected String access_token = null; private class PendingChanges implements Runnable { @@ -66,15 +68,16 @@ public void run() { } }; - public TDReplicator(TDDatabase db, URL remote, boolean continuous) { - this(db, remote, continuous, null); + public TDReplicator(TDDatabase db, URL remote, String access_token, boolean continuous) { + this(db, remote, access_token, continuous, null); } - public TDReplicator(TDDatabase db, URL remote, boolean continuous, + public TDReplicator(TDDatabase db, URL remote, String access_token, boolean continuous, HttpClientFactory clientFacotry) { this.db = db; this.remote = remote; + this.access_token = access_token; this.continuous = continuous; this.handler = db.getHandler(); diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java index b0c86b7..bf236dc 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java @@ -43,274 +43,301 @@ */ public class TDChangeTracker implements Runnable { - private URL databaseURL; - private TDChangeTrackerClient client; - private TDChangeTrackerMode mode; - private Object lastSequenceID; - - private Thread thread; - private boolean running = false; - private HttpUriRequest request; - - private String filterName; - private Map filterParams; - - private Throwable error; - - public enum TDChangeTrackerMode { - OneShot, LongPoll, Continuous - } - - public TDChangeTracker(URL databaseURL, TDChangeTrackerMode mode, - Object lastSequenceID, TDChangeTrackerClient client) { - this.databaseURL = databaseURL; - this.mode = mode; - this.lastSequenceID = lastSequenceID; - this.client = client; - } - - public void setFilterName(String filterName) { - this.filterName = filterName; - } - - public void setFilterParams(Map filterParams) { - this.filterParams = filterParams; - } - - public void setClient(TDChangeTrackerClient client) { - this.client = client; - } - - public String getDatabaseName() { - String result = null; - if (databaseURL != null) { - result = databaseURL.getPath(); - if (result != null) { - int pathLastSlashPos = result.lastIndexOf('/'); - if (pathLastSlashPos > 0) { - result = result.substring(pathLastSlashPos); - } - } - } - return result; - } - - public String getChangesFeedPath() { - String path = "_changes?feed="; - switch (mode) { - case OneShot: - path += "normal"; - break; - case LongPoll: - path += "longpoll&limit=50"; - break; - case Continuous: - path += "continuous"; - break; - } - path += "&heartbeat=300000"; - - if(lastSequenceID != null) { - path += "&since=" + URLEncoder.encode(lastSequenceID.toString()); - } - if(filterName != null) { - path += "&filter=" + URLEncoder.encode(filterName); - if(filterParams != null) { - for (String filterParamKey : filterParams.keySet()) { - path += "&" + URLEncoder.encode(filterParamKey) + "=" + URLEncoder.encode(filterParams.get(filterParamKey).toString()); - } - } - } - - return path; - } - - public URL getChangesFeedURL() { - String dbURLString = databaseURL.toExternalForm(); - if(!dbURLString.endsWith("/")) { - dbURLString += "/"; - } - dbURLString += getChangesFeedPath(); - URL result = null; - try { - result = new URL(dbURLString); - } catch(MalformedURLException e) { - Log.e(TDDatabase.TAG, "Changes feed ULR is malformed", e); - } - return result; - } - - @Override - public void run() { - running = true; - HttpClient httpClient = client.getHttpClient(); - while (running) { - - URL url = getChangesFeedURL(); - request = new HttpGet(url.toString()); - - // if the URL contains user info AND if this a DefaultHttpClient - // then preemptively set the auth credentials - if(url.getUserInfo() != null) { - if(url.getUserInfo().contains(":")) { - String[] userInfoSplit = url.getUserInfo().split(":"); - final Credentials creds = new UsernamePasswordCredentials(userInfoSplit[0], userInfoSplit[1]); - if(httpClient instanceof DefaultHttpClient) { - DefaultHttpClient dhc = (DefaultHttpClient)httpClient; - - HttpRequestInterceptor preemptiveAuth = new HttpRequestInterceptor() { - - @Override - public void process(HttpRequest request, - HttpContext context) throws HttpException, - IOException { - AuthState authState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE); - CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute( - ClientContext.CREDS_PROVIDER); - HttpHost targetHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); - - if (authState.getAuthScheme() == null) { - AuthScope authScope = new AuthScope(targetHost.getHostName(), targetHost.getPort()); - authState.setAuthScheme(new BasicScheme()); - authState.setCredentials(creds); - } - } - }; - - dhc.addRequestInterceptor(preemptiveAuth, 0); - } - } - else { - Log.w(TDDatabase.TAG, "Unable to parse user info, not setting credentials"); - } - } - - try { - String maskedRemoteWithoutCredentials = getChangesFeedURL().toString(); - maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials.replaceAll("://.*:.*@","://---:---@"); - Log.v(TDDatabase.TAG, "Making request to " + maskedRemoteWithoutCredentials); - HttpResponse response = httpClient.execute(request); - StatusLine status = response.getStatusLine(); - if(status.getStatusCode() >= 300) { - Log.e(TDDatabase.TAG, "Change tracker got error " + Integer.toString(status.getStatusCode())); - stop(); - } - HttpEntity entity = response.getEntity(); - if(entity != null) { - try { - InputStream input = entity.getContent(); - if(mode != TDChangeTrackerMode.Continuous) { - Map fullBody = TDServer.getObjectMapper().readValue(input, Map.class); - boolean responseOK = receivedPollResponse(fullBody); - if(mode == TDChangeTrackerMode.LongPoll && responseOK) { - Log.v(TDDatabase.TAG, "Starting new longpoll"); - continue; - } else { - Log.w(TDDatabase.TAG, "Change tracker calling stop"); - stop(); - } - } - else { - BufferedReader reader = new BufferedReader(new InputStreamReader(input)); - String line = null; - while ((line=reader.readLine()) != null) { - receivedChunk(line); - } - } - } finally { - try { entity.consumeContent(); } catch (IOException e){} - } - } - } catch (ClientProtocolException e) { - Log.e(TDDatabase.TAG, "ClientProtocolException in change tracker", e); - } catch (IOException e) { - if(running) { - //we get an exception when we're shutting down and have to - //close the socket underneath our read, ignore that - Log.e(TDDatabase.TAG, "IOException in change tracker", e); - } - } - } - Log.v(TDDatabase.TAG, "Change tracker run loop exiting"); - } - - public boolean receivedChunk(String line) { - if(line.length() > 1) { - try { - Map change = (Map)TDServer.getObjectMapper().readValue(line, Map.class); - if(!receivedChange(change)) { - Log.w(TDDatabase.TAG, String.format("Received unparseable change line from server: %s", line)); - return false; - } - } catch (Exception e) { - Log.w(TDDatabase.TAG, "Exception parsing JSON in change tracker", e); - return false; - } - } - return true; - } - - public boolean receivedChange(final Map change) { - Object seq = change.get("seq"); - if(seq == null) { - return false; - } - //pass the change to the client on the thread that created this change tracker - if(client != null) { - client.changeTrackerReceivedChange(change); - } - lastSequenceID = seq; - return true; - } - - public boolean receivedPollResponse(Map response) { - List> changes = (List)response.get("results"); - if(changes == null) { - return false; - } - for (Map change : changes) { - if(!receivedChange(change)) { - return false; - } - } - return true; - } - - public void setUpstreamError(String message) { - Log.w(TDDatabase.TAG, String.format("Server error: %s", message)); - this.error = new Throwable(message); - } - - public boolean start() { - this.error = null; - thread = new Thread(this, "ChangeTracker-" + databaseURL.toExternalForm()); - thread.start(); - return true; - } - - public void stop() { - Log.d(TDDatabase.TAG, "changed tracker asked to stop"); - running = false; - thread.interrupt(); - if(request != null) { - request.abort(); - } - - stopped(); - } - - public void stopped() { - Log.d(TDDatabase.TAG, "change tracker in stopped"); - if (client != null) { - Log.d(TDDatabase.TAG, "posting stopped"); - client.changeTrackerStopped(TDChangeTracker.this); - } - client = null; - Log.d(TDDatabase.TAG, "change tracker client should be null now"); - } - - public boolean isRunning() { - return running; - } + private URL databaseURL; + private TDChangeTrackerClient client; + private TDChangeTrackerMode mode; + private Object lastSequenceID; + + private Thread thread; + private boolean running = false; + private HttpUriRequest request; + + private String filterName; + private Map filterParams; + + private Throwable error; + + public enum TDChangeTrackerMode { + OneShot, LongPoll, Continuous + } + + public TDChangeTracker(URL databaseURL, TDChangeTrackerMode mode, + Object lastSequenceID, TDChangeTrackerClient client) { + this.databaseURL = databaseURL; + this.mode = mode; + this.lastSequenceID = lastSequenceID; + this.client = client; + } + + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + public void setFilterParams(Map filterParams) { + this.filterParams = filterParams; + } + + public void setClient(TDChangeTrackerClient client) { + this.client = client; + } + + public String getDatabaseName() { + String result = null; + if (databaseURL != null) { + result = databaseURL.getPath(); + if (result != null) { + int pathLastSlashPos = result.lastIndexOf('/'); + if (pathLastSlashPos > 0) { + result = result.substring(pathLastSlashPos); + } + } + } + return result; + } + + public String getChangesFeedPath() { + String path = "_changes?feed="; + switch (mode) { + case OneShot: + path += "normal"; + break; + case LongPoll: + path += "longpoll&limit=50"; + break; + case Continuous: + path += "continuous"; + break; + } + path += "&heartbeat=300000"; + + if (lastSequenceID != null) { + path += "&since=" + URLEncoder.encode(lastSequenceID.toString()); + } + if (filterName != null) { + path += "&filter=" + URLEncoder.encode(filterName); + if (filterParams != null) { + for (String filterParamKey : filterParams.keySet()) { + path += "&" + + URLEncoder.encode(filterParamKey) + + "=" + + URLEncoder.encode(filterParams + .get(filterParamKey).toString()); + } + } + } + return path; + } + + public URL getChangesFeedURL() { + String dbURLString = databaseURL.toExternalForm(); + if (!dbURLString.endsWith("/")) { + dbURLString += "/"; + } + dbURLString += getChangesFeedPath(); + URL result = null; + try { + result = new URL(dbURLString); + } catch (MalformedURLException e) { + Log.e(TDDatabase.TAG, "Changes feed ULR is malformed", e); + } + return result; + } + + @Override + public void run() { + running = true; + HttpClient httpClient = client.getHttpClient(); + while (running) { + + URL url = getChangesFeedURL(); + request = new HttpGet(url.toString()); + + // if the URL contains user info AND if this a DefaultHttpClient + // then preemptively set the auth credentials + if (url.getUserInfo() != null) { + if (url.getUserInfo().contains(":")) { + String[] userInfoSplit = url.getUserInfo().split(":"); + final Credentials creds = new UsernamePasswordCredentials( + userInfoSplit[0], userInfoSplit[1]); + if (httpClient instanceof DefaultHttpClient) { + DefaultHttpClient dhc = (DefaultHttpClient) httpClient; + + HttpRequestInterceptor preemptiveAuth = new HttpRequestInterceptor() { + + @Override + public void process(HttpRequest request, + HttpContext context) throws HttpException, + IOException { + AuthState authState = (AuthState) context + .getAttribute(ClientContext.TARGET_AUTH_STATE); + CredentialsProvider credsProvider = (CredentialsProvider) context + .getAttribute(ClientContext.CREDS_PROVIDER); + HttpHost targetHost = (HttpHost) context + .getAttribute(ExecutionContext.HTTP_TARGET_HOST); + + if (authState.getAuthScheme() == null) { + AuthScope authScope = new AuthScope( + targetHost.getHostName(), + targetHost.getPort()); + authState.setAuthScheme(new BasicScheme()); + authState.setCredentials(creds); + } + } + }; + + dhc.addRequestInterceptor(preemptiveAuth, 0); + } + } else { + Log.w(TDDatabase.TAG, + "Unable to parse user info, not setting credentials"); + } + } + + try { + String maskedRemoteWithoutCredentials = getChangesFeedURL() + .toString(); + maskedRemoteWithoutCredentials = maskedRemoteWithoutCredentials + .replaceAll("://.*:.*@", "://---:---@"); + Log.v(TDDatabase.TAG, "Making request to " + + maskedRemoteWithoutCredentials); + HttpResponse response = httpClient.execute(request); + StatusLine status = response.getStatusLine(); + if (status.getStatusCode() >= 300) { + Log.e(TDDatabase.TAG, + "Change tracker got error " + + Integer.toString(status.getStatusCode())); + stop(); + } + HttpEntity entity = response.getEntity(); + if (entity != null) { + try { + InputStream input = entity.getContent(); + if (mode != TDChangeTrackerMode.Continuous) { + Map fullBody = TDServer + .getObjectMapper().readValue(input, + Map.class); + boolean responseOK = receivedPollResponse(fullBody); + if (mode == TDChangeTrackerMode.LongPoll + && responseOK) { + Log.v(TDDatabase.TAG, "Starting new longpoll"); + continue; + } else { + Log.w(TDDatabase.TAG, + "Change tracker calling stop"); + stop(); + } + } else { + BufferedReader reader = new BufferedReader( + new InputStreamReader(input)); + String line = null; + while ((line = reader.readLine()) != null) { + receivedChunk(line); + } + } + } finally { + try { + entity.consumeContent(); + } catch (IOException e) { + } + } + } + } catch (ClientProtocolException e) { + Log.e(TDDatabase.TAG, + "ClientProtocolException in change tracker", e); + } catch (IOException e) { + if (running) { + // we get an exception when we're shutting down and have to + // close the socket underneath our read, ignore that + Log.e(TDDatabase.TAG, "IOException in change tracker", e); + } + } + } + Log.v(TDDatabase.TAG, "Change tracker run loop exiting"); + } + + public boolean receivedChunk(String line) { + if (line.length() > 1) { + try { + Map change = (Map) TDServer.getObjectMapper() + .readValue(line, Map.class); + if (!receivedChange(change)) { + Log.w(TDDatabase.TAG, String.format( + "Received unparseable change line from server: %s", + line)); + return false; + } + } catch (Exception e) { + Log.w(TDDatabase.TAG, + "Exception parsing JSON in change tracker", e); + return false; + } + } + return true; + } + + public boolean receivedChange(final Map change) { + Object seq = change.get("seq"); + if (seq == null) { + return false; + } + // pass the change to the client on the thread that created this change + // tracker + if (client != null) { + client.changeTrackerReceivedChange(change); + } + lastSequenceID = seq; + return true; + } + + public boolean receivedPollResponse(Map response) { + List> changes = (List) response.get("results"); + if (changes == null) { + return false; + } + for (Map change : changes) { + if (!receivedChange(change)) { + return false; + } + } + return true; + } + + public void setUpstreamError(String message) { + Log.w(TDDatabase.TAG, String.format("Server error: %s", message)); + this.error = new Throwable(message); + } + + public boolean start() { + this.error = null; + thread = new Thread(this, "ChangeTracker-" + + databaseURL.toExternalForm()); + thread.start(); + return true; + } + + public void stop() { + Log.d(TDDatabase.TAG, "changed tracker asked to stop"); + running = false; + thread.interrupt(); + if (request != null) { + request.abort(); + } + + stopped(); + } + + public void stopped() { + Log.d(TDDatabase.TAG, "change tracker in stopped"); + if (client != null) { + Log.d(TDDatabase.TAG, "posting stopped"); + client.changeTrackerStopped(TDChangeTracker.this); + } + client = null; + Log.d(TDDatabase.TAG, "change tracker client should be null now"); + } + + public boolean isRunning() { + return running; + } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java b/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java index ab62d4b..d85b5c7 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java @@ -512,6 +512,7 @@ public TDStatus do_POST_replicate(TDDatabase _db, String _docID, String _attachm boolean continuous = (continuousBoolean != null && continuousBoolean.booleanValue()); Boolean cancelBoolean = (Boolean)body.get("cancel"); boolean cancel = (cancelBoolean != null && cancelBoolean.booleanValue()); + String access_token = (String) ((Map)body.get("query_params")).get("access_token"); // Map the 'source' and 'target' JSON params to a local database and remote URL: if(source == null || target == null) { @@ -550,7 +551,7 @@ public TDStatus do_POST_replicate(TDDatabase _db, String _docID, String _attachm if(!cancel) { // Start replication: - TDReplicator repl = db.getReplicator(remote, server.getDefaultHttpClientFactory(), push, continuous); + TDReplicator repl = db.getReplicator(remote, server.getDefaultHttpClientFactory(), push, access_token, continuous); if(repl == null) { return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); } From 9600434fc01d9e05abb10400aaf6713de3b1db70 Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Fri, 24 May 2013 10:15:52 +0530 Subject: [PATCH 07/11] Fixed PullReplicator --- TouchDB-Android/.classpath | 4 +- .../src/com/couchbase/touchdb/TDDatabase.java | 5262 +++++++++-------- .../touchdb/replicator/TDPuller.java | 164 +- .../touchdb/replicator/TDPusher.java | 2 + .../touchdb/replicator/TDReplicator.java | 72 +- .../changetracker/TDChangeTracker.java | 36 +- 6 files changed, 2911 insertions(+), 2629 deletions(-) diff --git a/TouchDB-Android/.classpath b/TouchDB-Android/.classpath index f4aa6b2..0e46cdf 100644 --- a/TouchDB-Android/.classpath +++ b/TouchDB-Android/.classpath @@ -3,8 +3,8 @@ - - + + diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index 111c5ef..f88cb1b 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.net.URL; import java.util.ArrayList; +import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; @@ -53,2538 +54,2749 @@ */ public class TDDatabase extends Observable { - private String path; - private String name; - private SQLiteDatabase database; - private boolean open = false; - private int transactionLevel = 0; - public static final String TAG = "TDDatabase"; - - private Map views; - private Map filters; - private Map validations; - private List activeReplicators; - private TDBlobStore attachments; - - /** - * Options for what metadata to include in document bodies - */ - public enum TDContentOptions { - TDIncludeAttachments, TDIncludeConflicts, TDIncludeRevs, TDIncludeRevsInfo, TDIncludeLocalSeq, TDNoBody - } - - private static final Set KNOWN_SPECIAL_KEYS; - - static { - KNOWN_SPECIAL_KEYS = new HashSet(); - KNOWN_SPECIAL_KEYS.add("_id"); - KNOWN_SPECIAL_KEYS.add("_rev"); - KNOWN_SPECIAL_KEYS.add("_attachments"); - KNOWN_SPECIAL_KEYS.add("_deleted"); - KNOWN_SPECIAL_KEYS.add("_revisions"); - KNOWN_SPECIAL_KEYS.add("_revs_info"); - KNOWN_SPECIAL_KEYS.add("_conflicts"); - KNOWN_SPECIAL_KEYS.add("_deleted_conflicts"); - } - - public static final String SCHEMA = "" + - "CREATE TABLE docs ( " + - " doc_id INTEGER PRIMARY KEY, " + - " docid TEXT UNIQUE NOT NULL); " + - " CREATE INDEX docs_docid ON docs(docid); " + - " CREATE TABLE revs ( " + - " sequence INTEGER PRIMARY KEY AUTOINCREMENT, " + - " doc_id INTEGER NOT NULL REFERENCES docs(doc_id) ON DELETE CASCADE, " + - " revid TEXT NOT NULL, " + - " parent INTEGER REFERENCES revs(sequence) ON DELETE SET NULL, " + - " current BOOLEAN, " + - " deleted BOOLEAN DEFAULT 0, " + - " json BLOB); " + - " CREATE INDEX revs_by_id ON revs(revid, doc_id); " + - " CREATE INDEX revs_current ON revs(doc_id, current); " + - " CREATE INDEX revs_parent ON revs(parent); " + - " CREATE TABLE localdocs ( " + - " docid TEXT UNIQUE NOT NULL, " + - " revid TEXT NOT NULL, " + - " json BLOB); " + - " CREATE INDEX localdocs_by_docid ON localdocs(docid); " + - " CREATE TABLE views ( " + - " view_id INTEGER PRIMARY KEY, " + - " name TEXT UNIQUE NOT NULL," + - " version TEXT, " + - " lastsequence INTEGER DEFAULT 0); " + - " CREATE INDEX views_by_name ON views(name); " + - " CREATE TABLE maps ( " + - " view_id INTEGER NOT NULL REFERENCES views(view_id) ON DELETE CASCADE, " + - " sequence INTEGER NOT NULL REFERENCES revs(sequence) ON DELETE CASCADE, " + - " key TEXT NOT NULL COLLATE JSON, " + - " value TEXT); " + - " CREATE INDEX maps_keys on maps(view_id, key COLLATE JSON); " + - " CREATE TABLE attachments ( " + - " sequence INTEGER NOT NULL REFERENCES revs(sequence) ON DELETE CASCADE, " + - " filename TEXT NOT NULL, " + - " key BLOB NOT NULL, " + - " type TEXT, " + - " length INTEGER NOT NULL, " + - " revpos INTEGER DEFAULT 0); " + - " CREATE INDEX attachments_by_sequence on attachments(sequence, filename); " + - " CREATE TABLE replicators ( " + - " remote TEXT NOT NULL, " + - " push BOOLEAN, " + - " last_sequence TEXT, " + - " UNIQUE (remote, push)); " + - " PRAGMA user_version = 3"; // at the end, update user_version - - /*************************************************************************************************/ - /*** TDDatabase ***/ - /*************************************************************************************************/ - - public String getAttachmentStorePath() { - String attachmentStorePath = path; - int lastDotPosition = attachmentStorePath.lastIndexOf('.'); - if( lastDotPosition > 0 ) { - attachmentStorePath = attachmentStorePath.substring(0, lastDotPosition); - } - attachmentStorePath = attachmentStorePath + File.separator + "attachments"; - return attachmentStorePath; - } - - public static TDDatabase createEmptyDBAtPath(String path) { - if(!FileDirUtils.removeItemIfExists(path)) { - return null; - } - TDDatabase result = new TDDatabase(path); - File af = new File(result.getAttachmentStorePath()); - //recursively delete attachments path - if(!FileDirUtils.deleteRecursive(af)) { - return null; - } - if(!result.open()) { - return null; - } - return result; - } - - public TDDatabase(String path) { - assert(path.startsWith("/")); //path must be absolute - this.path = path; - this.name = FileDirUtils.getDatabaseNameFromPath(path); - } - - public String toString() { - return this.getClass().getName() + "[" + path + "]"; - } - - public boolean exists() { - return new File(path).exists(); - } - - /** - * Replaces the database with a copy of another database. - * - * This is primarily used to install a canned database on first launch of an app, in which case you should first check .exists to avoid replacing the database if it exists already. The canned database would have been copied into your app bundle at build time. - * - * @param databasePath Path of the database file that should replace this one. - * @param attachmentsPath Path of the associated attachments directory, or nil if there are no attachments. - * @return true if the database was copied, IOException if an error occurs - **/ - public boolean replaceWithDatabase(String databasePath, String attachmentsPath) throws IOException { - String dstAttachmentsPath = this.getAttachmentStorePath(); - File sourceFile = new File(databasePath); - File destFile = new File(path); - FileDirUtils.copyFile(sourceFile, destFile); - File attachmentsFile = new File(dstAttachmentsPath); - FileDirUtils.deleteRecursive(attachmentsFile); - attachmentsFile.mkdirs(); - if(attachmentsPath != null) { - FileDirUtils.copyFolder(new File(attachmentsPath), attachmentsFile); - } - return true; - } - - public boolean initialize(String statements) { - try { - for (String statement : statements.split(";")) { - database.execSQL(statement); - } - } catch (SQLException e) { - close(); - return false; - } - return true; - } - - public boolean open() { - if(open) { - return true; - } - - try { - database = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.CREATE_IF_NECESSARY); - TDCollateJSON.registerCustomCollators(database); - } - catch(SQLiteException e) { - Log.e(TDDatabase.TAG, "Error opening", e); - return false; - } - - // Stuff we need to initialize every time the database opens: - if(!initialize("PRAGMA foreign_keys = ON;")) { - Log.e(TDDatabase.TAG, "Error turning on foreign keys"); - return false; - } - - // Check the user_version number we last stored in the database: - int dbVersion = database.getVersion(); - - // Incompatible version changes increment the hundreds' place: - if(dbVersion >= 100) { - Log.w(TDDatabase.TAG, "TDDatabase: Database version (" + dbVersion + ") is newer than I know how to work with"); - database.close(); - return false; - } - - if(dbVersion < 1) { - // First-time initialization: - // (Note: Declaring revs.sequence as AUTOINCREMENT means the values will always be - // monotonically increasing, never reused. See ) - if(!initialize(SCHEMA)) { - database.close(); - return false; - } - dbVersion = 3; - } - - if (dbVersion < 2) { - // Version 2: added attachments.revpos - String upgradeSql = "ALTER TABLE attachments ADD COLUMN revpos INTEGER DEFAULT 0; " + - "PRAGMA user_version = 2"; - if(!initialize(upgradeSql)) { - database.close(); - return false; - } - dbVersion = 2; - } - - if (dbVersion < 3) { - String upgradeSql = "CREATE TABLE localdocs ( " + - "docid TEXT UNIQUE NOT NULL, " + - "revid TEXT NOT NULL, " + - "json BLOB); " + - "CREATE INDEX localdocs_by_docid ON localdocs(docid); " + - "PRAGMA user_version = 3"; - if(!initialize(upgradeSql)) { - database.close(); - return false; - } - dbVersion = 3; - } - - if (dbVersion < 4) { - String upgradeSql = "CREATE TABLE info ( " + - "key TEXT PRIMARY KEY, " + - "value TEXT); " + - "INSERT INTO INFO (key, value) VALUES ('privateUUID', '" + TDMisc.TDCreateUUID() + "'); " + - "INSERT INTO INFO (key, value) VALUES ('publicUUID', '" + TDMisc.TDCreateUUID() + "'); " + - "PRAGMA user_version = 4"; - if(!initialize(upgradeSql)) { - database.close(); - return false; - } - } - - if (dbVersion < 5) { - String upgradeSql = "CREATE TABLE replicator_log ( " + - " remote TEXT NOT NULL, " + - " push BOOLEAN, " + - " docid TEXT NOT NULL, " + - " revid TEXT NOT NULL, " + - " deleted BOOLEAN, " + - " sequence INTEGER, " + - " UNIQUE (remote, push, docid, revid)); " + - "PRAGMA user_version = 5"; - if(!initialize(upgradeSql)) { - database.close(); - return false; - } - } - - try { - attachments = new TDBlobStore(getAttachmentStorePath()); - } catch (IllegalArgumentException e) { - Log.e(TDDatabase.TAG, "Could not initialize attachment store", e); - database.close(); - return false; - } - - open = true; - return true; - } - - public boolean close() { - if(!open) { - return false; - } - - if(views != null) { - for (TDView view : views.values()) { - view.databaseClosing(); - } - } - views = null; - - if(activeReplicators != null) { - for(TDReplicator replicator : activeReplicators) { - replicator.databaseClosing(); - } - activeReplicators = null; - } - - if(database != null && database.isOpen()) { - database.close(); - } - open = false; - transactionLevel = 0; - return true; - } - - public boolean deleteDatabase() { - if(open) { - if(!close()) { - return false; - } - } - else if(!exists()) { - return true; - } - File file = new File(path); - File attachmentsFile = new File(getAttachmentStorePath()); - - boolean deleteStatus = file.delete(); - //recursively delete attachments path - boolean deleteAttachmentStatus = FileDirUtils.deleteRecursive(attachmentsFile); - return deleteStatus && deleteAttachmentStatus; - } - - public String getPath() { - return path; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - // Leave this package protected, so it can only be used - // TDView uses this accessor - SQLiteDatabase getDatabase() { - return database; - } - - public TDBlobStore getAttachments() { - return attachments; - } - - public long totalDataSize() { - File f = new File(path); - long size = f.length() + attachments.totalDataSize(); - return size; - } - - /** - * Begins a database transaction. Transactions can nest. - * Every beginTransaction() must be balanced by a later endTransaction() - */ - public boolean beginTransaction() { - try { - database.beginTransaction(); - ++transactionLevel; - //Log.v(TAG, "Begin transaction (level " + Integer.toString(transactionLevel) + ")..."); - } catch (SQLException e) { - return false; - } - return true; - } - - /** - * Commits or aborts (rolls back) a transaction. - * - * @param commit If true, commits; if false, aborts and rolls back, undoing all changes made since the matching -beginTransaction call, *including* any committed nested transactions. - */ - public boolean endTransaction(boolean commit) { - assert(transactionLevel > 0); - - if(commit) { - //Log.v(TAG, "Committing transaction (level " + Integer.toString(transactionLevel) + ")..."); - database.setTransactionSuccessful(); - database.endTransaction(); - } - else { - Log.v(TAG, "CANCEL transaction (level " + Integer.toString(transactionLevel) + ")..."); - try { - database.endTransaction(); - } catch (SQLException e) { - return false; - } - } - - --transactionLevel; - return true; - } - - /** - * Compacts the database storage by removing the bodies and attachments of obsolete revisions. - */ - public TDStatus compact() { - // Can't delete any rows because that would lose revision tree history. - // But we can remove the JSON of non-current revisions, which is most of the space. - try { - Log.v(TDDatabase.TAG, "Deleting JSON of old revisions..."); - ContentValues args = new ContentValues(); - args.put("json", (String)null); - database.update("revs", args, "current=0", null); - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error compacting", e); - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - Log.v(TDDatabase.TAG, "Deleting old attachments..."); - TDStatus result = garbageCollectAttachments(); - - Log.v(TDDatabase.TAG, "Vacuuming SQLite database..."); - try { - database.execSQL("VACUUM"); - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error vacuuming database", e); - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - return result; - } - - public String privateUUID() { - String result = null; - Cursor cursor = null; - try { - cursor = database.rawQuery("SELECT value FROM info WHERE key='privateUUID'", null); - if(cursor.moveToFirst()) { - result = cursor.getString(0); - } - } catch(SQLException e) { - Log.e(TAG, "Error querying privateUUID", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - return result; - } - - public String publicUUID() { - String result = null; - Cursor cursor = null; - try { - cursor = database.rawQuery("SELECT value FROM info WHERE key='publicUUID'", null); - if(cursor.moveToFirst()) { - result = cursor.getString(0); - } - } catch(SQLException e) { - Log.e(TAG, "Error querying privateUUID", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - return result; - } - - /** GETTING DOCUMENTS: **/ - - public int getDocumentCount() { - String sql = "SELECT COUNT(DISTINCT doc_id) FROM revs WHERE current=1 AND deleted=0"; - Cursor cursor = null; - int result = 0; - try { - cursor = database.rawQuery(sql, null); - if(cursor.moveToFirst()) { - result = cursor.getInt(0); - } - } catch(SQLException e) { - Log.e(TDDatabase.TAG, "Error getting document count", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - - return result; - } - - public long getLastSequence() { - String sql = "SELECT MAX(sequence) FROM revs"; - Cursor cursor = null; - long result = 0; - try { - cursor = database.rawQuery(sql, null); - if(cursor.moveToFirst()) { - result = cursor.getLong(0); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting last sequence", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - return result; - } - - /** Splices the contents of an NSDictionary into JSON data (that already represents a dict), without parsing the JSON. */ - public byte[] appendDictToJSON(byte[] json, Map dict) { - if(dict.size() == 0) { - return json; - } - - byte[] extraJSON = null; - try { - extraJSON = TDServer.getObjectMapper().writeValueAsBytes(dict); - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error convert extra JSON to bytes", e); - return null; - } - - int jsonLength = json.length; - int extraLength = extraJSON.length; - if(jsonLength == 2) { // Original JSON was empty - return extraJSON; - } - byte[] newJson = new byte[jsonLength + extraLength - 1]; - System.arraycopy(json, 0, newJson, 0, jsonLength - 1); // Copy json w/o trailing '}' - newJson[jsonLength - 1] = ','; // Add a ',' - System.arraycopy(extraJSON, 1, newJson, jsonLength, extraLength - 1); - return newJson; - } - - /** Inserts the _id, _rev and _attachments properties into the JSON data and stores it in rev. - Rev must already have its revID and sequence properties set. */ - public Map extraPropertiesForRevision(TDRevision rev, EnumSet contentOptions) { - - String docId = rev.getDocId(); - String revId = rev.getRevId(); - long sequenceNumber = rev.getSequence(); - assert(revId != null); - assert(sequenceNumber > 0); - - // Get attachment metadata, and optionally the contents: - boolean withAttachments = contentOptions.contains(TDContentOptions.TDIncludeAttachments); - Map attachmentsDict = getAttachmentsDictForSequenceWithContent(sequenceNumber, withAttachments); - - // Get more optional stuff to put in the properties: - //OPT: This probably ends up making redundant SQL queries if multiple options are enabled. - Long localSeq = null; - if(contentOptions.contains(TDContentOptions.TDIncludeLocalSeq)) { - localSeq = sequenceNumber; - } - - Map revHistory = null; - if(contentOptions.contains(TDContentOptions.TDIncludeRevs)) { - revHistory = getRevisionHistoryDict(rev); - } - - List revsInfo = null; - if(contentOptions.contains(TDContentOptions.TDIncludeRevsInfo)) { - revsInfo = new ArrayList(); - List revHistoryFull = getRevisionHistory(rev); - for (TDRevision historicalRev : revHistoryFull) { - Map revHistoryItem = new HashMap(); - String status = "available"; - if(historicalRev.isDeleted()) { - status = "deleted"; - } - // TODO: Detect missing revisions, set status="missing" - revHistoryItem.put("rev", historicalRev.getRevId()); - revHistoryItem.put("status", status); - revsInfo.add(revHistoryItem); - } - } - - List conflicts = null; - if(contentOptions.contains(TDContentOptions.TDIncludeConflicts)) { - TDRevisionList revs = getAllRevisionsOfDocumentID(docId, true); - if(revs.size() > 1) { - conflicts = new ArrayList(); - for (TDRevision historicalRev : revs) { - if(!historicalRev.equals(rev)) { - conflicts.add(historicalRev.getRevId()); - } - } - } - } - - Map result = new HashMap(); - result.put("_id", docId); - result.put("_rev", revId); - if(rev.isDeleted()) { - result.put("_deleted", true); - } - if(attachmentsDict != null) { - result.put("_attachments", attachmentsDict); - } - if(localSeq != null) { - result.put("_local_seq", localSeq); - } - if(revHistory != null) { - result.put("_revisions", revHistory); - } - if(revsInfo != null) { - result.put("_revs_info", revsInfo); - } - if(conflicts != null) { - result.put("_conflicts", conflicts); - } - - return result; - } - - /** Inserts the _id, _rev and _attachments properties into the JSON data and stores it in rev. - Rev must already have its revID and sequence properties set. */ - public void expandStoredJSONIntoRevisionWithAttachments(byte[] json, TDRevision rev, EnumSet contentOptions) { - Map extra = extraPropertiesForRevision(rev, contentOptions); - if(json != null) { - rev.setJson(appendDictToJSON(json, extra)); - } - else { - rev.setProperties(extra); - } - } - - @SuppressWarnings("unchecked") - public Map documentPropertiesFromJSON(byte[] json, String docId, String revId, long sequence, EnumSet contentOptions) { - - TDRevision rev = new TDRevision(docId, revId, false); - rev.setSequence(sequence); - Map extra = extraPropertiesForRevision(rev, contentOptions); - if(json == null) { - return extra; - } - - Map docProperties = null; - try { - docProperties = TDServer.getObjectMapper().readValue(json, Map.class); - docProperties.putAll(extra); - return docProperties; - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error serializing properties to JSON", e); - } - - return docProperties; - } - - public TDRevision getDocumentWithIDAndRev(String id, String rev, EnumSet contentOptions) { - TDRevision result = null; - String sql; - - Cursor cursor = null; - try { - cursor = null; - String cols = "revid, deleted, sequence"; - if(!contentOptions.contains(TDContentOptions.TDNoBody)) { - cols += ", json"; - } - if(rev != null) { - sql = "SELECT " + cols + " FROM revs, docs WHERE docs.docid=? AND revs.doc_id=docs.doc_id AND revid=? LIMIT 1"; - String[] args = {id, rev}; - cursor = database.rawQuery(sql, args); - } - else { - sql = "SELECT " + cols + " FROM revs, docs WHERE docs.docid=? AND revs.doc_id=docs.doc_id and current=1 and deleted=0 ORDER BY revid DESC LIMIT 1"; - String[] args = {id}; - cursor = database.rawQuery(sql, args); - } - - if(cursor.moveToFirst()) { - if(rev == null) { - rev = cursor.getString(0); - } - boolean deleted = (cursor.getInt(1) > 0); - result = new TDRevision(id, rev, deleted); - result.setSequence(cursor.getLong(2)); - if(!contentOptions.equals(EnumSet.of(TDContentOptions.TDNoBody))) { - byte[] json = null; - if(!contentOptions.contains(TDContentOptions.TDNoBody)) { - json = cursor.getBlob(3); - } - expandStoredJSONIntoRevisionWithAttachments(json, result, contentOptions); - } - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting document with id and rev", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - return result; - } - - public boolean existsDocumentWithIDAndRev(String docId, String revId) { - return getDocumentWithIDAndRev(docId, revId, EnumSet.of(TDContentOptions.TDNoBody)) != null; - } - - public TDStatus loadRevisionBody(TDRevision rev, EnumSet contentOptions) { - if(rev.getBody() != null) { - return new TDStatus(TDStatus.OK); - } - assert((rev.getDocId() != null) && (rev.getRevId() != null)); - - Cursor cursor = null; - TDStatus result = new TDStatus(TDStatus.NOT_FOUND); - try { - String sql = "SELECT sequence, json FROM revs, docs WHERE revid=? AND docs.docid=? AND revs.doc_id=docs.doc_id LIMIT 1"; - String[] args = { rev.getRevId(), rev.getDocId()}; - cursor = database.rawQuery(sql, args); - if(cursor.moveToFirst()) { - result.setCode(TDStatus.OK); - rev.setSequence(cursor.getLong(0)); - expandStoredJSONIntoRevisionWithAttachments(cursor.getBlob(1), rev, contentOptions); - } - } catch(SQLException e) { - Log.e(TDDatabase.TAG, "Error loading revision body", e); - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } finally { - if(cursor != null) { - cursor.close(); - } - } - return result; - } - - public long getDocNumericID(String docId) { - Cursor cursor = null; - String[] args = { docId }; - - long result = -1; - try { - cursor = database.rawQuery("SELECT doc_id FROM docs WHERE docid=?", args); - - if(cursor.moveToFirst()) { - result = cursor.getLong(0); - } - else { - result = 0; - } - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error getting doc numeric id", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - - return result; - } - - /** HISTORY: **/ - - /** - * Returns all the known revisions (or all current/conflicting revisions) of a document. - */ - public TDRevisionList getAllRevisionsOfDocumentID(String docId, long docNumericID, boolean onlyCurrent) { - - String sql = null; - if(onlyCurrent) { - sql = "SELECT sequence, revid, deleted FROM revs " + - "WHERE doc_id=? AND current ORDER BY sequence DESC"; - } - else { - sql = "SELECT sequence, revid, deleted FROM revs " + - "WHERE doc_id=? ORDER BY sequence DESC"; - } - - String[] args = { Long.toString(docNumericID) }; - Cursor cursor = null; - - cursor = database.rawQuery(sql, args); - - TDRevisionList result; - try { - cursor.moveToFirst(); - result = new TDRevisionList(); - while(!cursor.isAfterLast()) { - TDRevision rev = new TDRevision(docId, cursor.getString(1), (cursor.getInt(2) > 0)); - rev.setSequence(cursor.getLong(0)); - result.add(rev); - cursor.moveToNext(); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting all revisions of document", e); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - - return result; - } - - public TDRevisionList getAllRevisionsOfDocumentID(String docId, boolean onlyCurrent) { - long docNumericId = getDocNumericID(docId); - if(docNumericId < 0) { - return null; - } - else if(docNumericId == 0) { - return new TDRevisionList(); - } - else { - return getAllRevisionsOfDocumentID(docId, docNumericId, onlyCurrent); - } - } - - public List getConflictingRevisionIDsOfDocID(String docID) { - long docIdNumeric = getDocNumericID(docID); - if(docIdNumeric < 0) { - return null; - } - - List result = new ArrayList(); - Cursor cursor = null; - try { - String[] args = { Long.toString(docIdNumeric) }; - cursor = database.rawQuery("SELECT revid FROM revs WHERE doc_id=? AND current " + - "ORDER BY revid DESC OFFSET 1", args); - cursor.moveToFirst(); - while(!cursor.isAfterLast()) { - result.add(cursor.getString(0)); - cursor.moveToNext(); - } - - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting all revisions of document", e); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - - return result; - } - - public String findCommonAncestorOf(TDRevision rev, List revIDs) { - String result = null; - - if (revIDs.size() == 0) - return null; - String docId = rev.getDocId(); - long docNumericID = getDocNumericID(docId); - if (docNumericID <= 0) - return null; - String quotedRevIds = joinQuoted(revIDs); - String sql = "SELECT revid FROM revs " + - "WHERE doc_id=? and revid in (" + quotedRevIds + ") and revid <= ? " + - "ORDER BY revid DESC LIMIT 1"; - String[] args = { Long.toString(docNumericID) }; - - Cursor cursor = null; - try { - cursor = database.rawQuery(sql, args); - cursor.moveToFirst(); - if(!cursor.isAfterLast()) { - result = cursor.getString(0); - } - - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting all revisions of document", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - - return result; - } - - /** - * Returns an array of TDRevs in reverse chronological order, starting with the given revision. - */ - public List getRevisionHistory(TDRevision rev) { - String docId = rev.getDocId(); - String revId = rev.getRevId(); - assert((docId != null) && (revId != null)); - - long docNumericId = getDocNumericID(docId); - if(docNumericId < 0) { - return null; - } - else if(docNumericId == 0) { - return new ArrayList(); - } - - String sql = "SELECT sequence, parent, revid, deleted FROM revs " + - "WHERE doc_id=? ORDER BY sequence DESC"; - String[] args = { Long.toString(docNumericId) }; - Cursor cursor = null; - - List result; - try { - cursor = database.rawQuery(sql, args); - - cursor.moveToFirst(); - long lastSequence = 0; - result = new ArrayList(); - while(!cursor.isAfterLast()) { - long sequence = cursor.getLong(0); - boolean matches = false; - if(lastSequence == 0) { - matches = revId.equals(cursor.getString(2)); - } - else { - matches = (sequence == lastSequence); - } - if(matches) { - revId = cursor.getString(2); - boolean deleted = (cursor.getInt(3) > 0); - TDRevision aRev = new TDRevision(docId, revId, deleted); - aRev.setSequence(cursor.getLong(0)); - result.add(aRev); - lastSequence = cursor.getLong(1); - if(lastSequence == 0) { - break; - } - } - cursor.moveToNext(); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting revision history", e); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - - return result; - } - - // Splits a revision ID into its generation number and opaque suffix string - public static int parseRevIDNumber(String rev) { - int result = -1; - int dashPos = rev.indexOf("-"); - if(dashPos >= 0) { - try { - result = Integer.parseInt(rev.substring(0, dashPos)); - } catch (NumberFormatException e) { - // ignore, let it return -1 - } - } - return result; - } - - // Splits a revision ID into its generation number and opaque suffix string - public static String parseRevIDSuffix(String rev) { - String result = null; - int dashPos = rev.indexOf("-"); - if(dashPos >= 0) { - result = rev.substring(dashPos + 1); - } - return result; - } - - public static Map makeRevisionHistoryDict(List history) { - if(history == null) { - return null; - } - - // Try to extract descending numeric prefixes: - List suffixes = new ArrayList(); - int start = -1; - int lastRevNo = -1; - for (TDRevision rev : history) { - int revNo = parseRevIDNumber(rev.getRevId()); - String suffix = parseRevIDSuffix(rev.getRevId()); - if(revNo > 0 && suffix.length() > 0) { - if(start < 0) { - start = revNo; - } - else if(revNo != lastRevNo - 1) { - start = -1; - break; - } - lastRevNo = revNo; - suffixes.add(suffix); - } - else { - start = -1; - break; - } - } - - Map result = new HashMap(); - if(start == -1) { - // we failed to build sequence, just stuff all the revs in list - suffixes = new ArrayList(); - for (TDRevision rev : history) { - suffixes.add(rev.getRevId()); - } - } - else { - result.put("start", start); - } - result.put("ids", suffixes); - - return result; - } - - /** - * Returns the revision history as a _revisions dictionary, as returned by the REST API's ?revs=true option. - */ - public Map getRevisionHistoryDict(TDRevision rev) { - return makeRevisionHistoryDict(getRevisionHistory(rev)); - } - - public TDRevisionList changesSince(long lastSeq, TDChangesOptions options, TDFilterBlock filter) { - // http://wiki.apache.org/couchdb/HTTP_database_API#Changes - if(options == null) { - options = new TDChangesOptions(); - } - - boolean includeDocs = options.isIncludeDocs() || (filter != null); - String additionalSelectColumns = ""; - if(includeDocs) { - additionalSelectColumns = ", json"; - } - - String sql = "SELECT sequence, revs.doc_id, docid, revid, deleted" + additionalSelectColumns + " FROM revs, docs " - + "WHERE sequence > ? AND current=1 " - + "AND revs.doc_id = docs.doc_id " - + "ORDER BY revs.doc_id, revid DESC"; - String[] args = {Long.toString(lastSeq)}; - Cursor cursor = null; - TDRevisionList changes = null; - - try { - cursor = database.rawQuery(sql, args); - cursor.moveToFirst(); - changes = new TDRevisionList(); - long lastDocId = 0; - while(!cursor.isAfterLast()) { - if(!options.isIncludeConflicts()) { - // Only count the first rev for a given doc (the rest will be losing conflicts): - long docNumericId = cursor.getLong(1); - if(docNumericId == lastDocId) { - cursor.moveToNext(); - continue; - } - lastDocId = docNumericId; - } - - TDRevision rev = new TDRevision(cursor.getString(2), cursor.getString(3), (cursor.getInt(4) > 0)); - rev.setSequence(cursor.getLong(0)); - if(includeDocs) { - expandStoredJSONIntoRevisionWithAttachments(cursor.getBlob(5), rev, options.getContentOptions()); - } - if((filter == null) || (filter.filter(rev))) { - changes.add(rev); - } - cursor.moveToNext(); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error looking for changes", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - - if(options.isSortBySequence()) { - changes.sortBySequence(); - } - changes.limit(options.getLimit()); - return changes; - } - - /** - * Define or clear a named filter function. - * - * These aren't used directly by TDDatabase, but they're looked up by TDRouter when a _changes request has a ?filter parameter. - */ - public void defineFilter(String filterName, TDFilterBlock filter) { - if(filters == null) { - filters = new HashMap(); - } - filters.put(filterName, filter); - } - - public TDFilterBlock getFilterNamed(String filterName) { - TDFilterBlock result = null; - if(filters != null) { - result = filters.get(filterName); - } - return result; - } - - /** VIEWS: **/ - - public TDView registerView(TDView view) { - if(view == null) { - return null; - } - if(views == null) { - views = new HashMap(); - } - views.put(view.getName(), view); - return view; - } - - public TDView getViewNamed(String name) { - TDView view = null; - if(views != null) { - view = views.get(name); - } - if(view != null) { - return view; - } - return registerView(new TDView(this, name)); - } - - public TDView getExistingViewNamed(String name) { - TDView view = null; - if(views != null) { - view = views.get(name); - } - if(view != null) { - return view; - } - view = new TDView(this, name); - if(view.getViewId() == 0) { - return null; - } - - return registerView(view); - } - - public List getAllViews() { - Cursor cursor = null; - List result = null; - - try { - cursor = database.rawQuery("SELECT name FROM views", null); - cursor.moveToFirst(); - result = new ArrayList(); - while(!cursor.isAfterLast()) { - result.add(getViewNamed(cursor.getString(0))); - cursor.moveToNext(); - } - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error getting all views", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - - return result; - } - - public TDStatus deleteViewNamed(String name) { - TDStatus result = new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - try { - String[] whereArgs = { name }; - int rowsAffected = database.delete("views", "name=?", whereArgs); - if(rowsAffected > 0) { - result.setCode(TDStatus.OK); - } - else { - result.setCode(TDStatus.NOT_FOUND); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error deleting view", e); - } - return result; - } - - //FIX: This has a lot of code in common with -[TDView queryWithOptions:status:]. Unify the two! - public Map getDocsWithIDs(List docIDs, TDQueryOptions options) { - if(options == null) { - options = new TDQueryOptions(); - } - - long updateSeq = 0; - if(options.isUpdateSeq()) { - updateSeq = getLastSequence(); // TODO: needs to be atomic with the following SELECT - } - - // Generate the SELECT statement, based on the options: - String additionalCols = ""; - if(options.isIncludeDocs()) { - additionalCols = ", json, sequence"; - } - String sql = "SELECT revs.doc_id, docid, revid, deleted" + additionalCols + " FROM revs, docs WHERE"; - - if(docIDs != null) { - sql += " docid IN (" + joinQuoted(docIDs) + ")"; - } else { - sql += " deleted=0"; - } - - sql += " AND current=1 AND docs.doc_id = revs.doc_id"; - - List argsList = new ArrayList(); - Object minKey = options.getStartKey(); - Object maxKey = options.getEndKey(); - boolean inclusiveMin = true; - boolean inclusiveMax = options.isInclusiveEnd(); - if(options.isDescending()) { - minKey = maxKey; - maxKey = options.getStartKey(); - inclusiveMin = inclusiveMax; - inclusiveMax = true; - } - - if(minKey != null) { - assert(minKey instanceof String); - if(inclusiveMin) { - sql += " AND docid >= ?"; - } else { - sql += " AND docid > ?"; - } - argsList.add((String)minKey); - } - - if(maxKey != null) { - assert(maxKey instanceof String); - if(inclusiveMax) { - sql += " AND docid <= ?"; - } - else { - sql += " AND docid < ?"; - } - argsList.add((String)maxKey); - } - - - String order = "ASC"; - if(options.isDescending()) { - order = "DESC"; - } - - sql += " ORDER BY docid " + order + ", revid DESC LIMIT ? OFFSET ?"; - - argsList.add(Integer.toString(options.getLimit())); - argsList.add(Integer.toString(options.getSkip())); - Cursor cursor = null; - long lastDocID = 0; - List> rows = null; - - try { - cursor = database.rawQuery(sql, argsList.toArray(new String[argsList.size()])); - - cursor.moveToFirst(); - rows = new ArrayList>(); - while(!cursor.isAfterLast()) { - long docNumericID = cursor.getLong(0); - if(docNumericID == lastDocID) { - cursor.moveToNext(); - continue; - } - lastDocID = docNumericID; - - String docId = cursor.getString(1); - String revId = cursor.getString(2); - Map docContents = null; - boolean deleted = cursor.getInt(3) > 0; - if(options.isIncludeDocs() && !deleted) { - byte[] json = cursor.getBlob(4); - long sequence = cursor.getLong(5); - docContents = documentPropertiesFromJSON(json, docId, revId, sequence, options.getContentOptions()); - } - - Map valueMap = new HashMap(); - valueMap.put("rev", revId); - - Map change = new HashMap(); - change.put("id", docId); - change.put("key", docId); - change.put("value", valueMap); - if(docContents != null) { - change.put("doc", docContents); - } - if(deleted) { - change.put("deleted", true); - } - - rows.add(change); - - cursor.moveToNext(); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting all docs", e); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - - int totalRows = cursor.getCount(); //??? Is this true, or does it ignore limit/offset? - Map result = new HashMap(); - result.put("rows", rows); - result.put("total_rows", totalRows); - result.put("offset", options.getSkip()); - if(updateSeq != 0) { - result.put("update_seq", updateSeq); - } - - - return result; - } - - public Map getAllDocs(TDQueryOptions options) { - return getDocsWithIDs(null, options); - } - - /*************************************************************************************************/ - /*** TDDatabase+Attachments ***/ - /*************************************************************************************************/ - - public TDStatus insertAttachmentForSequenceWithNameAndType(InputStream contentStream, long sequence, String name, String contentType, int revpos) { - assert(sequence > 0); - assert(name != null); - - TDBlobKey key = new TDBlobKey(); - if(!attachments.storeBlobStream(contentStream, key)) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - byte[] keyData = key.getBytes(); - try { - ContentValues args = new ContentValues(); - args.put("sequence", sequence); - args.put("filename", name); - args.put("key", keyData); - args.put("type", contentType); - args.put("length", attachments.getSizeOfBlob(key)); - args.put("revpos", revpos); - database.insert("attachments", null, args); - return new TDStatus(TDStatus.CREATED); - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error inserting attachment", e); - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - } - - public TDStatus copyAttachmentNamedFromSequenceToSequence(String name, long fromSeq, long toSeq) { - assert(name != null); - assert(toSeq > 0); - if(fromSeq < 0) { - return new TDStatus(TDStatus.NOT_FOUND); - } - - Cursor cursor = null; - - String[] args = { Long.toString(toSeq), name, Long.toString(fromSeq), name }; - try { - database.execSQL("INSERT INTO attachments (sequence, filename, key, type, length, revpos) " + - "SELECT ?, ?, key, type, length, revpos FROM attachments " + - "WHERE sequence=? AND filename=?", args); - cursor = database.rawQuery("SELECT changes()", null); - cursor.moveToFirst(); - int rowsUpdated = cursor.getInt(0); - if(rowsUpdated == 0) { - // Oops. This means a glitch in our attachment-management or pull code, - // or else a bug in the upstream server. - Log.w(TDDatabase.TAG, "Can't find inherited attachment " + name + " from seq# " + Long.toString(fromSeq) + " to copy to " + Long.toString(toSeq)); - return new TDStatus(TDStatus.NOT_FOUND); - } - else { - return new TDStatus(TDStatus.OK); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error copying attachment", e); - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } finally { - if(cursor != null) { - cursor.close(); - } - } - } - - /** - * Returns the content and MIME type of an attachment - */ - public TDAttachment getAttachmentForSequence(long sequence, String filename, TDStatus status) { - assert(sequence > 0); - assert(filename != null); - - - Cursor cursor = null; - - String[] args = { Long.toString(sequence), filename }; - try { - cursor = database.rawQuery("SELECT key, type FROM attachments WHERE sequence=? AND filename=?", args); - - if(!cursor.moveToFirst()) { - status.setCode(TDStatus.NOT_FOUND); - return null; - } - - byte[] keyData = cursor.getBlob(0); - //TODO add checks on key here? (ios version) - TDBlobKey key = new TDBlobKey(keyData); - InputStream contentStream = attachments.blobStreamForKey(key); - if(contentStream == null) { - Log.e(TDDatabase.TAG, "Failed to load attachment"); - status.setCode(TDStatus.INTERNAL_SERVER_ERROR); - return null; - } - else { - status.setCode(TDStatus.OK); - TDAttachment result = new TDAttachment(); - result.setContentStream(contentStream); - result.setContentType(cursor.getString(1)); - return result; - } - - - } catch (SQLException e) { - status.setCode(TDStatus.INTERNAL_SERVER_ERROR); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - - } - - /** - * Constructs an "_attachments" dictionary for a revision, to be inserted in its JSON body. - */ - public Map getAttachmentsDictForSequenceWithContent(long sequence, boolean withContent) { - assert(sequence > 0); - - Cursor cursor = null; - - String args[] = { Long.toString(sequence) }; - try { - cursor = database.rawQuery("SELECT filename, key, type, length, revpos FROM attachments WHERE sequence=?", args); - - if(!cursor.moveToFirst()) { - return null; - } - - Map result = new HashMap(); - - while(!cursor.isAfterLast()) { - - byte[] keyData = cursor.getBlob(1); - TDBlobKey key = new TDBlobKey(keyData); - String digestString = "sha1-" + Base64.encodeBytes(keyData); - String dataBase64 = null; - if(withContent) { - byte[] data = attachments.blobForKey(key); - if(data != null) { - dataBase64 = Base64.encodeBytes(data); - } - else { - Log.w(TDDatabase.TAG, "Error loading attachment"); - } - } - - Map attachment = new HashMap(); - if(dataBase64 == null) { - attachment.put("stub", true); - } - else { - attachment.put("data", dataBase64); - } - attachment.put("digest", digestString); - attachment.put("content_type", cursor.getString(2)); - attachment.put("length", cursor.getInt(3)); - attachment.put("revpos", cursor.getInt(4)); - - result.put(cursor.getString(0), attachment); - - cursor.moveToNext(); - } - - return result; - - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting attachments for sequence", e); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - } - - /** - * Modifies a TDRevision's body by changing all attachments with revpos < minRevPos into stubs. - * - * @param rev - * @param minRevPos - */ - public void stubOutAttachmentsIn(TDRevision rev, int minRevPos) - { - if (minRevPos <= 1) { - return; - } - Map properties = (Map)rev.getProperties(); - Map attachments = null; - if(properties != null) { - attachments = (Map)properties.get("_attachments"); - } - Map editedProperties = null; - Map editedAttachments = null; - for (String name : attachments.keySet()) { - Map attachment = (Map)attachments.get(name); - int revPos = (Integer) attachment.get("revpos"); - Object stub = attachment.get("stub"); - if (revPos > 0 && revPos < minRevPos && (stub == null)) { - // Strip this attachment's body. First make its dictionary mutable: - if (editedProperties == null) { - editedProperties = new HashMap(properties); - editedAttachments = new HashMap(attachments); - editedProperties.put("_attachments", editedAttachments); - } - // ...then remove the 'data' and 'follows' key: - Map editedAttachment = new HashMap(attachment); - editedAttachment.remove("data"); - editedAttachment.remove("follows"); - editedAttachment.put("stub", true); - editedAttachments.put(name,editedAttachment); - Log.d(TDDatabase.TAG, "Stubbed out attachment" + rev + " " + name + ": revpos" + revPos + " " + minRevPos); - } - } - if (editedProperties != null) - rev.setProperties(editedProperties); - } - - /** - * Given a newly-added revision, adds the necessary attachment rows to the database and stores inline attachments into the blob store. - */ - public TDStatus processAttachmentsForRevision(TDRevision rev, long parentSequence) { - assert(rev != null); - long newSequence = rev.getSequence(); - assert(newSequence > parentSequence); - - // If there are no attachments in the new rev, there's nothing to do: - Map newAttachments = null; - Map properties = (Map)rev.getProperties(); - if(properties != null) { - newAttachments = (Map)properties.get("_attachments"); - } - if(newAttachments == null || newAttachments.size() == 0 || rev.isDeleted()) { - return new TDStatus(TDStatus.OK); - } - - for (String name : newAttachments.keySet()) { - - TDStatus status = new TDStatus(); - Map newAttach = (Map)newAttachments.get(name); - String newContentBase64 = (String)newAttach.get("data"); - if(newContentBase64 != null) { - // New item contains data, so insert it. First decode the data: - byte[] newContents; - try { - newContents = Base64.decode(newContentBase64); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "IOExeption parsing base64", e); - return new TDStatus(TDStatus.BAD_REQUEST); - } - if(newContents == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - // Now determine the revpos, i.e. generation # this was added in. Usually this is - // implicit, but a rev being pulled in replication will have it set already. - int generation = rev.getGeneration(); - assert(generation > 0); - Object revposObj = newAttach.get("revpos"); - int revpos = generation; - if(revposObj != null && revposObj instanceof Integer) { - revpos = ((Integer)revposObj).intValue(); - } - - if(revpos > generation) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - // Finally insert the attachment: - status = insertAttachmentForSequenceWithNameAndType(new ByteArrayInputStream(newContents), newSequence, name, (String)newAttach.get("content_type"), revpos); - } - else { - // It's just a stub, so copy the previous revision's attachment entry: - //? Should I enforce that the type and digest (if any) match? - status = copyAttachmentNamedFromSequenceToSequence(name, parentSequence, newSequence); - } - if(!status.isSuccessful()) { - return status; - } - } - - return new TDStatus(TDStatus.OK); - } - - /** - * Updates or deletes an attachment, creating a new document revision in the process. - * Used by the PUT / DELETE methods called on attachment URLs. - */ - public TDRevision updateAttachment(String filename, InputStream contentStream, String contentType, String docID, String oldRevID, TDStatus status) { - status.setCode(TDStatus.BAD_REQUEST); - if(filename == null || filename.length() == 0 || (contentStream != null && contentType == null) || (oldRevID != null && docID == null) || (contentStream != null && docID == null)) { - return null; - } - - beginTransaction(); - try { - TDRevision oldRev = new TDRevision(docID, oldRevID, false); - if(oldRevID != null) { - // Load existing revision if this is a replacement: - TDStatus loadStatus = loadRevisionBody(oldRev, EnumSet.noneOf(TDContentOptions.class)); - status.setCode(loadStatus.getCode()); - if(!status.isSuccessful()) { - if(status.getCode() == TDStatus.NOT_FOUND && existsDocumentWithIDAndRev(docID, null)) { - status.setCode(TDStatus.CONFLICT); // if some other revision exists, it's a conflict - } - return null; - } - - Map attachments = (Map) oldRev.getProperties().get("_attachments"); - if(contentStream == null && attachments != null && !attachments.containsKey(filename)) { - status.setCode(TDStatus.NOT_FOUND); - return null; - } - // Remove the _attachments stubs so putRevision: doesn't copy the rows for me - // OPT: Would be better if I could tell loadRevisionBody: not to add it - if(attachments != null) { - Map properties = new HashMap(oldRev.getProperties()); - properties.remove("_attachments"); - oldRev.setBody(new TDBody(properties)); - } - } else { - // If this creates a new doc, it needs a body: - oldRev.setBody(new TDBody(new HashMap())); - } - - // Create a new revision: - TDRevision newRev = putRevision(oldRev, oldRevID, false, status); - if(newRev == null) { - return null; - } - - if(oldRevID != null) { - // Copy all attachment rows _except_ for the one being updated: - String[] args = { Long.toString(newRev.getSequence()), Long.toString(oldRev.getSequence()), filename }; - database.execSQL("INSERT INTO attachments " - + "(sequence, filename, key, type, length, revpos) " - + "SELECT ?, filename, key, type, length, revpos FROM attachments " - + "WHERE sequence=? AND filename != ?", args); - } - - if(contentStream != null) { - // If not deleting, add a new attachment entry: - TDStatus insertStatus = insertAttachmentForSequenceWithNameAndType(contentStream, newRev.getSequence(), - filename, contentType, newRev.getGeneration()); - status.setCode(insertStatus.getCode()); - - if(!status.isSuccessful()) { - return null; - } - } - - status.setCode((contentStream != null) ? TDStatus.CREATED : TDStatus.OK); - return newRev; - - } catch(SQLException e) { - Log.e(TAG, "Error uploading attachment", e); - status.setCode(TDStatus.INTERNAL_SERVER_ERROR); - return null; - } finally { - endTransaction(status.isSuccessful()); - } - } - - /** - * Deletes obsolete attachments from the database and blob store. - */ - public TDStatus garbageCollectAttachments() { - // First delete attachment rows for already-cleared revisions: - // OPT: Could start after last sequence# we GC'd up to - - try { - database.execSQL("DELETE FROM attachments WHERE sequence IN " + - "(SELECT sequence from revs WHERE json IS null)"); - } - catch(SQLException e) { - Log.e(TDDatabase.TAG, "Error deleting attachments", e); - } - - // Now collect all remaining attachment IDs and tell the store to delete all but these: - Cursor cursor = null; - try { - cursor = database.rawQuery("SELECT DISTINCT key FROM attachments", null); - - cursor.moveToFirst(); - List allKeys = new ArrayList(); - while(!cursor.isAfterLast()) { - TDBlobKey key = new TDBlobKey(cursor.getBlob(0)); - allKeys.add(key); - cursor.moveToNext(); - } - - int numDeleted = attachments.deleteBlobsExceptWithKeys(allKeys); - if(numDeleted < 0) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - Log.v(TDDatabase.TAG, "Deleted " + numDeleted + " attachments"); - - return new TDStatus(TDStatus.OK); - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error finding attachment keys in use", e); - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } finally { - if(cursor != null) { - cursor.close(); - } - } - } - - /*************************************************************************************************/ - /*** TDDatabase+Insertion ***/ - /*************************************************************************************************/ - - /** DOCUMENT & REV IDS: **/ - - public static boolean isValidDocumentId(String id) { - // http://wiki.apache.org/couchdb/HTTP_Document_API#Documents - if(id == null || id.length() == 0) { - return false; - } - if(id.charAt(0) == '_') { - return (id.startsWith("_design/")); - } - return true; - // "_local/*" is not a valid document ID. Local docs have their own API and shouldn't get here. - } - - public static String generateDocumentId() { - return TDMisc.TDCreateUUID(); - } - - public String generateNextRevisionID(String revisionId) { - // Revision IDs have a generation count, a hyphen, and a UUID. - int generation = 0; - if(revisionId != null) { - generation = TDRevision.generationFromRevID(revisionId); - if(generation == 0) { - return null; - } - } - String digest = TDMisc.TDCreateUUID(); //TODO: Generate canonical digest of body - return Integer.toString(generation + 1) + "-" + digest; - } - - public long insertDocumentID(String docId) { - long rowId = -1; - try { - ContentValues args = new ContentValues(); - args.put("docid", docId); - rowId = database.insert("docs", null, args); - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error inserting document id", e); - } - return rowId; - } - - public long getOrInsertDocNumericID(String docId) { - long docNumericId = getDocNumericID(docId); - if(docNumericId == 0) { - docNumericId = insertDocumentID(docId); - } - return docNumericId; - } - - /** - * Parses the _revisions dict from a document into an array of revision ID strings - */ - public static List parseCouchDBRevisionHistory(Map docProperties) { - Map revisions = (Map)docProperties.get("_revisions"); - if(revisions == null) { - return null; - } - List revIDs = (List)revisions.get("ids"); - Integer start = (Integer)revisions.get("start"); - if(start != null) { - for(int i=0; i < revIDs.size(); i++) { - String revID = revIDs.get(i); - revIDs.set(i, Integer.toString(start--) + "-" + revID); - } - } - return revIDs; - } - - /** INSERTION: **/ - - public byte[] encodeDocumentJSON(TDRevision rev) { - - Map origProps = rev.getProperties(); - if(origProps == null) { - return null; - } - - // Don't allow any "_"-prefixed keys. Known ones we'll ignore, unknown ones are an error. - Map properties = new HashMap(origProps.size()); - for (String key : origProps.keySet()) { - if(key.startsWith("_")) { - if(!KNOWN_SPECIAL_KEYS.contains(key)) { - Log.e(TAG, "TDDatabase: Invalid top-level key '" + key + "' in document to be inserted"); - return null; - } - } else { - properties.put(key, origProps.get(key)); - } - } - - byte[] json = null; - try { - json = TDServer.getObjectMapper().writeValueAsBytes(properties); - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error serializing " + rev + " to JSON", e); - } - return json; - } - - public void notifyChange(TDRevision rev, URL source) { - Map changeNotification = new HashMap(); - changeNotification.put("rev", rev); - changeNotification.put("seq", rev.getSequence()); - if(source != null) { - changeNotification.put("source", source); - } - setChanged(); - notifyObservers(changeNotification); - } - - public long insertRevision(TDRevision rev, long docNumericID, long parentSequence, boolean current, byte[] data) { - long rowId = 0; - try { - ContentValues args = new ContentValues(); - args.put("doc_id", docNumericID); - args.put("revid", rev.getRevId()); - if(parentSequence != 0) { - args.put("parent", parentSequence); - } - args.put("current", current); - args.put("deleted", rev.isDeleted()); - args.put("json", data); - rowId = database.insert("revs", null, args); - rev.setSequence(rowId); - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error inserting revision", e); - } - return rowId; - } - - private TDRevision putRevision(TDRevision rev, String prevRevId, TDStatus resultStatus) { - return putRevision(rev, prevRevId, false, resultStatus); - } - - /** - * Stores a new (or initial) revision of a document. - * - * This is what's invoked by a PUT or POST. As with those, the previous revision ID must be supplied when necessary and the call will fail if it doesn't match. - * - * @param rev The revision to add. If the docID is null, a new UUID will be assigned. Its revID must be null. It must have a JSON body. - * @param prevRevId The ID of the revision to replace (same as the "?rev=" parameter to a PUT), or null if this is a new document. - * @param allowConflict If false, an error status 409 will be returned if the insertion would create a conflict, i.e. if the previous revision already has a child. - * @param resultStatus On return, an HTTP status code indicating success or failure. - * @return A new TDRevision with the docID, revID and sequence filled in (but no body). - */ - @SuppressWarnings("unchecked") - public TDRevision putRevision(TDRevision rev, String prevRevId, boolean allowConflict, TDStatus resultStatus) { - // prevRevId is the rev ID being replaced, or nil if an insert - String docId = rev.getDocId(); - boolean deleted = rev.isDeleted(); - if((rev == null) || ((prevRevId != null) && (docId == null)) || (deleted && (docId == null)) - || ((docId != null) && !isValidDocumentId(docId))) { - resultStatus.setCode(TDStatus.BAD_REQUEST); - return null; - } - - resultStatus.setCode(TDStatus.INTERNAL_SERVER_ERROR); - beginTransaction(); - Cursor cursor = null; - - //// PART I: In which are performed lookups and validations prior to the insert... - - long docNumericID = (docId != null) ? getDocNumericID(docId) : 0; - long parentSequence = 0; - try { - if(prevRevId != null) { - // Replacing: make sure given prevRevID is current & find its sequence number: - if(docNumericID <= 0) { - resultStatus.setCode(TDStatus.NOT_FOUND); - return null; - } - - String[] args = {Long.toString(docNumericID), prevRevId}; - String additionalWhereClause = ""; - if(!allowConflict) { - additionalWhereClause = "AND current=1"; - } - - cursor = database.rawQuery("SELECT sequence FROM revs WHERE doc_id=? AND revid=? " + additionalWhereClause + " LIMIT 1", args); - - if(cursor.moveToFirst()) { - parentSequence = cursor.getLong(0); - } - - if(parentSequence == 0) { - // Not found: either a 404 or a 409, depending on whether there is any current revision - if(!allowConflict && existsDocumentWithIDAndRev(docId, null)) { - resultStatus.setCode(TDStatus.CONFLICT); - return null; - } - else { - resultStatus.setCode(TDStatus.NOT_FOUND); - return null; - } - } - - if(validations != null && validations.size() > 0) { - // Fetch the previous revision and validate the new one against it: - TDRevision prevRev = new TDRevision(docId, prevRevId, false); - TDStatus status = validateRevision(rev, prevRev); - if(!status.isSuccessful()) { - resultStatus.setCode(status.getCode()); - return null; - } - } - - // Make replaced rev non-current: - ContentValues updateContent = new ContentValues(); - updateContent.put("current", 0); - database.update("revs", updateContent, "sequence=" + parentSequence, null); - } - else { - // Inserting first revision. - if(deleted && (docId != null)) { - // Didn't specify a revision to delete: 404 or a 409, depending - if(existsDocumentWithIDAndRev(docId, null)) { - resultStatus.setCode(TDStatus.CONFLICT); - return null; - } - else { - resultStatus.setCode(TDStatus.NOT_FOUND); - return null; - } - } - - // Validate: - TDStatus status = validateRevision(rev, null); - if(!status.isSuccessful()) { - resultStatus.setCode(status.getCode()); - return null; - } - - if(docId != null) { - // Inserting first revision, with docID given (PUT): - if(docNumericID <= 0) { - // Doc doesn't exist at all; create it: - docNumericID = insertDocumentID(docId); - if(docNumericID <= 0) { - return null; - } - } else { - // Doc exists; check whether current winning revision is deleted: - String[] args = { Long.toString(docNumericID) }; - cursor = database.rawQuery("SELECT sequence, deleted FROM revs WHERE doc_id=? and current=1 ORDER BY revid DESC LIMIT 1", args); - - if(cursor.moveToFirst()) { - boolean wasAlreadyDeleted = (cursor.getInt(1) > 0); - if(wasAlreadyDeleted) { - // Make the deleted revision no longer current: - ContentValues updateContent = new ContentValues(); - updateContent.put("current", 0); - database.update("revs", updateContent, "sequence=" + cursor.getLong(0), null); - } - else if (!allowConflict) { - // docId already exists, current not deleted, conflict - resultStatus.setCode(TDStatus.CONFLICT); - return null; - } - } - } - } - else { - // Inserting first revision, with no docID given (POST): generate a unique docID: - docId = TDDatabase.generateDocumentId(); - docNumericID = insertDocumentID(docId); - if(docNumericID <= 0) { - return null; - } - } - } - - //// PART II: In which insertion occurs... - - // Bump the revID and update the JSON: - String newRevId = generateNextRevisionID(prevRevId); - byte[] data = null; - if(!rev.isDeleted()) { - data = encodeDocumentJSON(rev); - if(data == null) { - // bad or missing json - resultStatus.setCode(TDStatus.BAD_REQUEST); - return null; - } - } - - rev = rev.copyWithDocID(docId, newRevId); - - // Now insert the rev itself: - long newSequence = insertRevision(rev, docNumericID, parentSequence, true, data); - if(newSequence == 0) { - return null; - } - - // Store any attachments: - if(attachments != null) { - TDStatus status = processAttachmentsForRevision(rev, parentSequence); - if(!status.isSuccessful()) { - resultStatus.setCode(status.getCode()); - return null; - } - } - - // Success! - if(deleted) { - resultStatus.setCode(TDStatus.OK); - } - else { - resultStatus.setCode(TDStatus.CREATED); - } - - } catch (SQLException e1) { - Log.e(TDDatabase.TAG, "Error putting revision", e1); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - endTransaction(resultStatus.isSuccessful()); - } - - //// EPILOGUE: A change notification is sent... - notifyChange(rev, null); - return rev; - } - - /** - * Inserts an already-existing revision replicated from a remote database. - * - * It must already have a revision ID. This may create a conflict! The revision's history must be given; ancestor revision IDs that don't already exist locally will create phantom revisions with no content. - */ - public TDStatus forceInsert(TDRevision rev, List revHistory, URL source) { - - String docId = rev.getDocId(); - String revId = rev.getRevId(); - if(!isValidDocumentId(docId) || (revId == null)) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - int historyCount = revHistory.size(); - if(historyCount == 0) { - revHistory = new ArrayList(); - revHistory.add(revId); - historyCount = 1; - } else if(!revHistory.get(0).equals(rev.getRevId())) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - boolean success = false; - beginTransaction(); - try { - // First look up all locally-known revisions of this document: - long docNumericID = getOrInsertDocNumericID(docId); - TDRevisionList localRevs = getAllRevisionsOfDocumentID(docId, docNumericID, false); - if(localRevs == null) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - // Walk through the remote history in chronological order, matching each revision ID to - // a local revision. When the list diverges, start creating blank local revisions to fill - // in the local history: - long sequence = 0; - long localParentSequence = 0; - for(int i = revHistory.size() - 1; i >= 0; --i) { - revId = revHistory.get(i); - TDRevision localRev = localRevs.revWithDocIdAndRevId(docId, revId); - if(localRev != null) { - // This revision is known locally. Remember its sequence as the parent of the next one: - sequence = localRev.getSequence(); - assert(sequence > 0); - localParentSequence = sequence; - } - else { - // This revision isn't known, so add it: - TDRevision newRev; - byte[] data = null; - boolean current = false; - if(i == 0) { - // Hey, this is the leaf revision we're inserting: - newRev = rev; - if(!rev.isDeleted()) { - data = encodeDocumentJSON(rev); - if(data == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - } - current = true; - } - else { - // It's an intermediate parent, so insert a stub: - newRev = new TDRevision(docId, revId, false); - } - - // Insert it: - sequence = insertRevision(newRev, docNumericID, sequence, current, data); - - if(sequence <= 0) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - if(i == 0) { - // Write any changed attachments for the new revision: - TDStatus status = processAttachmentsForRevision(rev, localParentSequence); - if(!status.isSuccessful()) { - return status; - } - } - } - } - - // Mark the latest local rev as no longer current: - if(localParentSequence > 0 && localParentSequence != sequence) { - ContentValues args = new ContentValues(); - args.put("current", 0); - String[] whereArgs = { Long.toString(localParentSequence) }; - try { - database.update("revs", args, "sequence=?", whereArgs); - } catch (SQLException e) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - } - - success = true; - } catch(SQLException e) { - endTransaction(success); - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } finally { - endTransaction(success); - } - - // Notify and return: - notifyChange(rev, source); - return new TDStatus(TDStatus.CREATED); - } - - /** VALIDATION **/ - - /** - * Define or clear a named document validation function. - */ - public void defineValidation(String name, TDValidationBlock validationBlock) { - if(validations == null) { - validations = new HashMap(); - } - validations.put(name, validationBlock); - } - - public TDValidationBlock getValidationNamed(String name) { - TDValidationBlock result = null; - if(validations != null) { - result = validations.get(name); - } - return result; - } - - public TDStatus validateRevision(TDRevision newRev, TDRevision oldRev) { - TDStatus result = new TDStatus(TDStatus.OK); - if(validations == null || validations.size() == 0) { - return result; - } - TDValidationContextImpl context = new TDValidationContextImpl(this, oldRev); - for (String validationName : validations.keySet()) { - TDValidationBlock validation = getValidationNamed(validationName); - if(!validation.validate(newRev, context)) { - result.setCode(context.getErrorType().getCode()); - break; - } - } - return result; - } - - /*************************************************************************************************/ - /*** TDDatabase+Replication ***/ - /*************************************************************************************************/ - - //TODO implement missing replication methods - - public List getActiveReplicators() { - return activeReplicators; - } - - public TDReplicator getActiveReplicator(URL remote, boolean push) { - if(activeReplicators != null) { - for (TDReplicator replicator : activeReplicators) { - if(replicator.getRemote().equals(remote) && replicator.isPush() == push && replicator.isRunning()) { - return replicator; - } - } - } - return null; - } - - public TDReplicator getReplicator(URL remote, boolean push, String access_token, boolean continuous, ScheduledExecutorService workExecutor) { - TDReplicator replicator = getReplicator(remote, null, push, access_token, continuous, workExecutor); - return replicator; - } - - public TDReplicator getReplicator(URL remote, HttpClientFactory httpClientFactory, boolean push,String access_token, boolean continuous, ScheduledExecutorService workExecutor) { - TDReplicator result = getActiveReplicator(remote, push); - if(result != null) { - return result; - } - result = push ? new TDPusher(this, remote, access_token, continuous, httpClientFactory, workExecutor) : new TDPuller(this, remote, access_token, continuous, httpClientFactory, workExecutor); - - if(activeReplicators == null) { - activeReplicators = new ArrayList(); - } - activeReplicators.add(result); - return result; - } - - public TDRevisionList getPendingRevisions(URL url, boolean push){ - TDRevisionList result = new TDRevisionList(); - String[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0) }; - Cursor cursor = database.rawQuery("SELECT docid, revid, deleted, sequence FROM replicator_log WHERE remote=? AND push=? LIMIT 50", args); - if(cursor.moveToFirst()) { - do{ - TDRevision rev = new TDRevision(cursor.getString(0), cursor.getString(1), cursor.getInt(2) == 1? true : false); - rev.setSequence(cursor.getLong(3)); - result.add(rev); - } while(cursor.moveToNext()); - } - - if(cursor != null) { - cursor.close(); - } + private String path; + private String name; + private SQLiteDatabase database; + private boolean open = false; + private int transactionLevel = 0; + public static final String TAG = "TDDatabase"; + + private Map views; + private Map filters; + private Map validations; + private List activeReplicators; + private TDBlobStore attachments; + + /** + * Options for what metadata to include in document bodies + */ + public enum TDContentOptions { + TDIncludeAttachments, TDIncludeConflicts, TDIncludeRevs, TDIncludeRevsInfo, TDIncludeLocalSeq, TDNoBody + } + + private static final Set KNOWN_SPECIAL_KEYS; + + static { + KNOWN_SPECIAL_KEYS = new HashSet(); + KNOWN_SPECIAL_KEYS.add("_id"); + KNOWN_SPECIAL_KEYS.add("_rev"); + KNOWN_SPECIAL_KEYS.add("_attachments"); + KNOWN_SPECIAL_KEYS.add("_deleted"); + KNOWN_SPECIAL_KEYS.add("_revisions"); + KNOWN_SPECIAL_KEYS.add("_revs_info"); + KNOWN_SPECIAL_KEYS.add("_conflicts"); + KNOWN_SPECIAL_KEYS.add("_deleted_conflicts"); + } + + public static final String SCHEMA = "" + + "CREATE TABLE docs ( " + + " doc_id INTEGER PRIMARY KEY, " + + " docid TEXT UNIQUE NOT NULL); " + + " CREATE INDEX docs_docid ON docs(docid); " + + " CREATE TABLE revs ( " + + " sequence INTEGER PRIMARY KEY AUTOINCREMENT, " + + " doc_id INTEGER NOT NULL REFERENCES docs(doc_id) ON DELETE CASCADE, " + + " revid TEXT NOT NULL, " + + " parent INTEGER REFERENCES revs(sequence) ON DELETE SET NULL, " + + " current BOOLEAN, " + + " deleted BOOLEAN DEFAULT 0, " + + " json BLOB); " + + " CREATE INDEX revs_by_id ON revs(revid, doc_id); " + + " CREATE INDEX revs_current ON revs(doc_id, current); " + + " CREATE INDEX revs_parent ON revs(parent); " + + " CREATE TABLE localdocs ( " + + " docid TEXT UNIQUE NOT NULL, " + + " revid TEXT NOT NULL, " + + " json BLOB); " + + " CREATE INDEX localdocs_by_docid ON localdocs(docid); " + + " CREATE TABLE views ( " + + " view_id INTEGER PRIMARY KEY, " + + " name TEXT UNIQUE NOT NULL," + + " version TEXT, " + + " lastsequence INTEGER DEFAULT 0); " + + " CREATE INDEX views_by_name ON views(name); " + + " CREATE TABLE maps ( " + + " view_id INTEGER NOT NULL REFERENCES views(view_id) ON DELETE CASCADE, " + + " sequence INTEGER NOT NULL REFERENCES revs(sequence) ON DELETE CASCADE, " + + " key TEXT NOT NULL COLLATE JSON, " + + " value TEXT); " + + " CREATE INDEX maps_keys on maps(view_id, key COLLATE JSON); " + + " CREATE TABLE attachments ( " + + " sequence INTEGER NOT NULL REFERENCES revs(sequence) ON DELETE CASCADE, " + + " filename TEXT NOT NULL, " + + " key BLOB NOT NULL, " + + " type TEXT, " + + " length INTEGER NOT NULL, " + + " revpos INTEGER DEFAULT 0); " + + " CREATE INDEX attachments_by_sequence on attachments(sequence, filename); " + + " CREATE TABLE replicators ( " + + " remote TEXT NOT NULL, " + " push BOOLEAN, " + + " last_sequence TEXT, " + + " UNIQUE (remote, push)); " + + " PRAGMA user_version = 3"; // at the end, update user_version + + /*************************************************************************************************/ + /*** TDDatabase ***/ + /*************************************************************************************************/ + + public String getAttachmentStorePath() { + String attachmentStorePath = path; + int lastDotPosition = attachmentStorePath.lastIndexOf('.'); + if (lastDotPosition > 0) { + attachmentStorePath = attachmentStorePath.substring(0, + lastDotPosition); + } + attachmentStorePath = attachmentStorePath + File.separator + + "attachments"; + return attachmentStorePath; + } + + public static TDDatabase createEmptyDBAtPath(String path) { + if (!FileDirUtils.removeItemIfExists(path)) { + return null; + } + TDDatabase result = new TDDatabase(path); + File af = new File(result.getAttachmentStorePath()); + // recursively delete attachments path + if (!FileDirUtils.deleteRecursive(af)) { + return null; + } + if (!result.open()) { + return null; + } return result; - } - - public boolean logRevision(URL url, boolean push, TDRevision rev){ - boolean success = false; - Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), rev.getDocId(), rev.getRevId(), (rev.isDeleted()?1:0), rev.getSequence()}; - Cursor cursor = null; + } + + public TDDatabase(String path) { + assert (path.startsWith("/")); // path must be absolute + this.path = path; + this.name = FileDirUtils.getDatabaseNameFromPath(path); + } + + public String toString() { + return this.getClass().getName() + "[" + path + "]"; + } + + public boolean exists() { + return new File(path).exists(); + } + + /** + * Replaces the database with a copy of another database. + * + * This is primarily used to install a canned database on first launch of an + * app, in which case you should first check .exists to avoid replacing the + * database if it exists already. The canned database would have been copied + * into your app bundle at build time. + * + * @param databasePath + * Path of the database file that should replace this one. + * @param attachmentsPath + * Path of the associated attachments directory, or nil if there + * are no attachments. + * @return true if the database was copied, IOException if an error occurs + **/ + public boolean replaceWithDatabase(String databasePath, + String attachmentsPath) throws IOException { + String dstAttachmentsPath = this.getAttachmentStorePath(); + File sourceFile = new File(databasePath); + File destFile = new File(path); + FileDirUtils.copyFile(sourceFile, destFile); + File attachmentsFile = new File(dstAttachmentsPath); + FileDirUtils.deleteRecursive(attachmentsFile); + attachmentsFile.mkdirs(); + if (attachmentsPath != null) { + FileDirUtils.copyFolder(new File(attachmentsPath), attachmentsFile); + } + return true; + } + + public boolean initialize(String statements) { try { - database.execSQL("INSERT INTO replicator_log(remote, push, docid, revid, deleted, sequence) VALUES(?,?,?,?,?,?)", args); - cursor = database.rawQuery("SELECT changes()", null); - if(cursor.moveToFirst() && cursor.getInt(0)>0){ - success = true; - } - }catch(SQLiteConstraintException e){ - // Trying to log a revision that is already present - // Do nothing - success = true; - } finally { - if(cursor != null) { - cursor.close(); - } - } - return success; - } - - public void removeLogForRevision(URL url, boolean push, TDRevision rev){ - Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), rev.getDocId(), rev.getRevId()}; - Cursor cursor = null; + for (String statement : statements.split(";")) { + database.execSQL(statement); + } + } catch (SQLException e) { + close(); + return false; + } + return true; + } + + public boolean open() { + if (open) { + return true; + } + + try { + database = SQLiteDatabase.openDatabase(path, null, + SQLiteDatabase.CREATE_IF_NECESSARY); + TDCollateJSON.registerCustomCollators(database); + } catch (SQLiteException e) { + Log.e(TDDatabase.TAG, "Error opening", e); + return false; + } + + // Stuff we need to initialize every time the database opens: + if (!initialize("PRAGMA foreign_keys = ON;")) { + Log.e(TDDatabase.TAG, "Error turning on foreign keys"); + return false; + } + + // Check the user_version number we last stored in the database: + int dbVersion = database.getVersion(); + + // Incompatible version changes increment the hundreds' place: + if (dbVersion >= 100) { + Log.w(TDDatabase.TAG, "TDDatabase: Database version (" + dbVersion + + ") is newer than I know how to work with"); + database.close(); + return false; + } + + if (dbVersion < 1) { + // First-time initialization: + // (Note: Declaring revs.sequence as AUTOINCREMENT means the values + // will always be + // monotonically increasing, never reused. See + // ) + if (!initialize(SCHEMA)) { + database.close(); + return false; + } + dbVersion = 3; + } + + if (dbVersion < 2) { + // Version 2: added attachments.revpos + String upgradeSql = "ALTER TABLE attachments ADD COLUMN revpos INTEGER DEFAULT 0; " + + "PRAGMA user_version = 2"; + if (!initialize(upgradeSql)) { + database.close(); + return false; + } + dbVersion = 2; + } + + if (dbVersion < 3) { + String upgradeSql = "CREATE TABLE localdocs ( " + + "docid TEXT UNIQUE NOT NULL, " + "revid TEXT NOT NULL, " + + "json BLOB); " + + "CREATE INDEX localdocs_by_docid ON localdocs(docid); " + + "PRAGMA user_version = 3"; + if (!initialize(upgradeSql)) { + database.close(); + return false; + } + dbVersion = 3; + } + + if (dbVersion < 4) { + String upgradeSql = "CREATE TABLE info ( " + + "key TEXT PRIMARY KEY, " + "value TEXT); " + + "INSERT INTO INFO (key, value) VALUES ('privateUUID', '" + + TDMisc.TDCreateUUID() + "'); " + + "INSERT INTO INFO (key, value) VALUES ('publicUUID', '" + + TDMisc.TDCreateUUID() + "'); " + + "PRAGMA user_version = 4"; + if (!initialize(upgradeSql)) { + database.close(); + return false; + } + } + + if (dbVersion < 5) { + String upgradeSql = "CREATE TABLE replicator_log ( " + + " remote TEXT NOT NULL, " + + " push BOOLEAN, " + + " docid TEXT NOT NULL, " + + " revid TEXT NOT NULL, " + + " deleted BOOLEAN, " + + " sequence INTEGER, " + + " lastUpdated INTEGER, " + + " UNIQUE (remote, push, docid, revid)); " + + "PRAGMA user_version = 5"; + if (!initialize(upgradeSql)) { + database.close(); + return false; + } + } + + try { + attachments = new TDBlobStore(getAttachmentStorePath()); + } catch (IllegalArgumentException e) { + Log.e(TDDatabase.TAG, "Could not initialize attachment store", e); + database.close(); + return false; + } + + open = true; + return true; + } + + public boolean close() { + if (!open) { + return false; + } + + if (views != null) { + for (TDView view : views.values()) { + view.databaseClosing(); + } + } + views = null; + + if (activeReplicators != null) { + for (TDReplicator replicator : activeReplicators) { + replicator.databaseClosing(); + } + activeReplicators = null; + } + + if (database != null && database.isOpen()) { + database.close(); + } + open = false; + transactionLevel = 0; + return true; + } + + public boolean deleteDatabase() { + if (open) { + if (!close()) { + return false; + } + } else if (!exists()) { + return true; + } + File file = new File(path); + File attachmentsFile = new File(getAttachmentStorePath()); + + boolean deleteStatus = file.delete(); + // recursively delete attachments path + boolean deleteAttachmentStatus = FileDirUtils + .deleteRecursive(attachmentsFile); + return deleteStatus && deleteAttachmentStatus; + } + + public String getPath() { + return path; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + // Leave this package protected, so it can only be used + // TDView uses this accessor + SQLiteDatabase getDatabase() { + return database; + } + + public TDBlobStore getAttachments() { + return attachments; + } + + public long totalDataSize() { + File f = new File(path); + long size = f.length() + attachments.totalDataSize(); + return size; + } + + /** + * Begins a database transaction. Transactions can nest. Every + * beginTransaction() must be balanced by a later endTransaction() + */ + public boolean beginTransaction() { + try { + database.beginTransaction(); + ++transactionLevel; + // Log.v(TAG, "Begin transaction (level " + + // Integer.toString(transactionLevel) + ")..."); + } catch (SQLException e) { + return false; + } + return true; + } + + /** + * Commits or aborts (rolls back) a transaction. + * + * @param commit + * If true, commits; if false, aborts and rolls back, undoing all + * changes made since the matching -beginTransaction call, + * *including* any committed nested transactions. + */ + public boolean endTransaction(boolean commit) { + assert (transactionLevel > 0); + + if (commit) { + // Log.v(TAG, "Committing transaction (level " + + // Integer.toString(transactionLevel) + ")..."); + database.setTransactionSuccessful(); + database.endTransaction(); + } else { + Log.v(TAG, + "CANCEL transaction (level " + + Integer.toString(transactionLevel) + ")..."); + try { + database.endTransaction(); + } catch (SQLException e) { + return false; + } + } + + --transactionLevel; + return true; + } + + /** + * Compacts the database storage by removing the bodies and attachments of + * obsolete revisions. + */ + public TDStatus compact() { + // Can't delete any rows because that would lose revision tree history. + // But we can remove the JSON of non-current revisions, which is most of + // the space. + try { + Log.v(TDDatabase.TAG, "Deleting JSON of old revisions..."); + ContentValues args = new ContentValues(); + args.put("json", (String) null); + database.update("revs", args, "current=0", null); + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error compacting", e); + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + Log.v(TDDatabase.TAG, "Deleting old attachments..."); + TDStatus result = garbageCollectAttachments(); + + Log.v(TDDatabase.TAG, "Vacuuming SQLite database..."); + try { + database.execSQL("VACUUM"); + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error vacuuming database", e); + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + return result; + } + + public String privateUUID() { + String result = null; + Cursor cursor = null; + try { + cursor = database.rawQuery( + "SELECT value FROM info WHERE key='privateUUID'", null); + if (cursor.moveToFirst()) { + result = cursor.getString(0); + } + } catch (SQLException e) { + Log.e(TAG, "Error querying privateUUID", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + + public String publicUUID() { + String result = null; + Cursor cursor = null; + try { + cursor = database.rawQuery( + "SELECT value FROM info WHERE key='publicUUID'", null); + if (cursor.moveToFirst()) { + result = cursor.getString(0); + } + } catch (SQLException e) { + Log.e(TAG, "Error querying privateUUID", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + + /** GETTING DOCUMENTS: **/ + + public int getDocumentCount() { + String sql = "SELECT COUNT(DISTINCT doc_id) FROM revs WHERE current=1 AND deleted=0"; + Cursor cursor = null; + int result = 0; + try { + cursor = database.rawQuery(sql, null); + if (cursor.moveToFirst()) { + result = cursor.getInt(0); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting document count", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + public long getLastSequence() { + String sql = "SELECT MAX(sequence) FROM revs"; + Cursor cursor = null; + long result = 0; + try { + cursor = database.rawQuery(sql, null); + if (cursor.moveToFirst()) { + result = cursor.getLong(0); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting last sequence", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + + /** + * Splices the contents of an NSDictionary into JSON data (that already + * represents a dict), without parsing the JSON. + */ + public byte[] appendDictToJSON(byte[] json, Map dict) { + if (dict.size() == 0) { + return json; + } + + byte[] extraJSON = null; + try { + extraJSON = TDServer.getObjectMapper().writeValueAsBytes(dict); + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error convert extra JSON to bytes", e); + return null; + } + + int jsonLength = json.length; + int extraLength = extraJSON.length; + if (jsonLength == 2) { // Original JSON was empty + return extraJSON; + } + byte[] newJson = new byte[jsonLength + extraLength - 1]; + System.arraycopy(json, 0, newJson, 0, jsonLength - 1); // Copy json w/o + // trailing '}' + newJson[jsonLength - 1] = ','; // Add a ',' + System.arraycopy(extraJSON, 1, newJson, jsonLength, extraLength - 1); + return newJson; + } + + /** + * Inserts the _id, _rev and _attachments properties into the JSON data and + * stores it in rev. Rev must already have its revID and sequence properties + * set. + */ + public Map extraPropertiesForRevision(TDRevision rev, + EnumSet contentOptions) { + + String docId = rev.getDocId(); + String revId = rev.getRevId(); + long sequenceNumber = rev.getSequence(); + assert (revId != null); + assert (sequenceNumber > 0); + + // Get attachment metadata, and optionally the contents: + boolean withAttachments = contentOptions + .contains(TDContentOptions.TDIncludeAttachments); + Map attachmentsDict = getAttachmentsDictForSequenceWithContent( + sequenceNumber, withAttachments); + + // Get more optional stuff to put in the properties: + // OPT: This probably ends up making redundant SQL queries if multiple + // options are enabled. + Long localSeq = null; + if (contentOptions.contains(TDContentOptions.TDIncludeLocalSeq)) { + localSeq = sequenceNumber; + } + + Map revHistory = null; + if (contentOptions.contains(TDContentOptions.TDIncludeRevs)) { + revHistory = getRevisionHistoryDict(rev); + } + + List revsInfo = null; + if (contentOptions.contains(TDContentOptions.TDIncludeRevsInfo)) { + revsInfo = new ArrayList(); + List revHistoryFull = getRevisionHistory(rev); + for (TDRevision historicalRev : revHistoryFull) { + Map revHistoryItem = new HashMap(); + String status = "available"; + if (historicalRev.isDeleted()) { + status = "deleted"; + } + // TODO: Detect missing revisions, set status="missing" + revHistoryItem.put("rev", historicalRev.getRevId()); + revHistoryItem.put("status", status); + revsInfo.add(revHistoryItem); + } + } + + List conflicts = null; + if (contentOptions.contains(TDContentOptions.TDIncludeConflicts)) { + TDRevisionList revs = getAllRevisionsOfDocumentID(docId, true); + if (revs.size() > 1) { + conflicts = new ArrayList(); + for (TDRevision historicalRev : revs) { + if (!historicalRev.equals(rev)) { + conflicts.add(historicalRev.getRevId()); + } + } + } + } + + Map result = new HashMap(); + result.put("_id", docId); + result.put("_rev", revId); + if (rev.isDeleted()) { + result.put("_deleted", true); + } + if (attachmentsDict != null) { + result.put("_attachments", attachmentsDict); + } + if (localSeq != null) { + result.put("_local_seq", localSeq); + } + if (revHistory != null) { + result.put("_revisions", revHistory); + } + if (revsInfo != null) { + result.put("_revs_info", revsInfo); + } + if (conflicts != null) { + result.put("_conflicts", conflicts); + } + + return result; + } + + /** + * Inserts the _id, _rev and _attachments properties into the JSON data and + * stores it in rev. Rev must already have its revID and sequence properties + * set. + */ + public void expandStoredJSONIntoRevisionWithAttachments(byte[] json, + TDRevision rev, EnumSet contentOptions) { + Map extra = extraPropertiesForRevision(rev, + contentOptions); + if (json != null) { + rev.setJson(appendDictToJSON(json, extra)); + } else { + rev.setProperties(extra); + } + } + + @SuppressWarnings("unchecked") + public Map documentPropertiesFromJSON(byte[] json, + String docId, String revId, long sequence, + EnumSet contentOptions) { + + TDRevision rev = new TDRevision(docId, revId, false); + rev.setSequence(sequence); + Map extra = extraPropertiesForRevision(rev, + contentOptions); + if (json == null) { + return extra; + } + + Map docProperties = null; + try { + docProperties = TDServer.getObjectMapper().readValue(json, + Map.class); + docProperties.putAll(extra); + return docProperties; + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error serializing properties to JSON", e); + } + + return docProperties; + } + + public TDRevision getDocumentWithIDAndRev(String id, String rev, + EnumSet contentOptions) { + TDRevision result = null; + String sql; + + Cursor cursor = null; + try { + cursor = null; + String cols = "revid, deleted, sequence"; + if (!contentOptions.contains(TDContentOptions.TDNoBody)) { + cols += ", json"; + } + if (rev != null) { + sql = "SELECT " + + cols + + " FROM revs, docs WHERE docs.docid=? AND revs.doc_id=docs.doc_id AND revid=? LIMIT 1"; + String[] args = { id, rev }; + cursor = database.rawQuery(sql, args); + } else { + sql = "SELECT " + + cols + + " FROM revs, docs WHERE docs.docid=? AND revs.doc_id=docs.doc_id and current=1 and deleted=0 ORDER BY revid DESC LIMIT 1"; + String[] args = { id }; + cursor = database.rawQuery(sql, args); + } + + if (cursor.moveToFirst()) { + if (rev == null) { + rev = cursor.getString(0); + } + boolean deleted = (cursor.getInt(1) > 0); + result = new TDRevision(id, rev, deleted); + result.setSequence(cursor.getLong(2)); + if (!contentOptions.equals(EnumSet + .of(TDContentOptions.TDNoBody))) { + byte[] json = null; + if (!contentOptions.contains(TDContentOptions.TDNoBody)) { + json = cursor.getBlob(3); + } + expandStoredJSONIntoRevisionWithAttachments(json, result, + contentOptions); + } + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting document with id and rev", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + + public boolean existsDocumentWithIDAndRev(String docId, String revId) { + return getDocumentWithIDAndRev(docId, revId, + EnumSet.of(TDContentOptions.TDNoBody)) != null; + } + + public TDStatus loadRevisionBody(TDRevision rev, + EnumSet contentOptions) { + if (rev.getBody() != null) { + return new TDStatus(TDStatus.OK); + } + assert ((rev.getDocId() != null) && (rev.getRevId() != null)); + + Cursor cursor = null; + TDStatus result = new TDStatus(TDStatus.NOT_FOUND); + try { + String sql = "SELECT sequence, json FROM revs, docs WHERE revid=? AND docs.docid=? AND revs.doc_id=docs.doc_id LIMIT 1"; + String[] args = { rev.getRevId(), rev.getDocId() }; + cursor = database.rawQuery(sql, args); + if (cursor.moveToFirst()) { + result.setCode(TDStatus.OK); + rev.setSequence(cursor.getLong(0)); + expandStoredJSONIntoRevisionWithAttachments(cursor.getBlob(1), + rev, contentOptions); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error loading revision body", e); + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + + public long getDocNumericID(String docId) { + Cursor cursor = null; + String[] args = { docId }; + + long result = -1; + try { + cursor = database.rawQuery("SELECT doc_id FROM docs WHERE docid=?", + args); + + if (cursor.moveToFirst()) { + result = cursor.getLong(0); + } else { + result = 0; + } + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error getting doc numeric id", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + /** HISTORY: **/ + + /** + * Returns all the known revisions (or all current/conflicting revisions) of + * a document. + */ + public TDRevisionList getAllRevisionsOfDocumentID(String docId, + long docNumericID, boolean onlyCurrent) { + + String sql = null; + if (onlyCurrent) { + sql = "SELECT sequence, revid, deleted FROM revs " + + "WHERE doc_id=? AND current ORDER BY sequence DESC"; + } else { + sql = "SELECT sequence, revid, deleted FROM revs " + + "WHERE doc_id=? ORDER BY sequence DESC"; + } + + String[] args = { Long.toString(docNumericID) }; + Cursor cursor = null; + + cursor = database.rawQuery(sql, args); + + TDRevisionList result; + try { + cursor.moveToFirst(); + result = new TDRevisionList(); + while (!cursor.isAfterLast()) { + TDRevision rev = new TDRevision(docId, cursor.getString(1), + (cursor.getInt(2) > 0)); + rev.setSequence(cursor.getLong(0)); + result.add(rev); + cursor.moveToNext(); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting all revisions of document", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + public TDRevisionList getAllRevisionsOfDocumentID(String docId, + boolean onlyCurrent) { + long docNumericId = getDocNumericID(docId); + if (docNumericId < 0) { + return null; + } else if (docNumericId == 0) { + return new TDRevisionList(); + } else { + return getAllRevisionsOfDocumentID(docId, docNumericId, onlyCurrent); + } + } + + public List getConflictingRevisionIDsOfDocID(String docID) { + long docIdNumeric = getDocNumericID(docID); + if (docIdNumeric < 0) { + return null; + } + + List result = new ArrayList(); + Cursor cursor = null; + try { + String[] args = { Long.toString(docIdNumeric) }; + cursor = database.rawQuery( + "SELECT revid FROM revs WHERE doc_id=? AND current " + + "ORDER BY revid DESC OFFSET 1", args); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + result.add(cursor.getString(0)); + cursor.moveToNext(); + } + + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting all revisions of document", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + public String findCommonAncestorOf(TDRevision rev, List revIDs) { + String result = null; + + if (revIDs.size() == 0) + return null; + String docId = rev.getDocId(); + long docNumericID = getDocNumericID(docId); + if (docNumericID <= 0) + return null; + String quotedRevIds = joinQuoted(revIDs); + String sql = "SELECT revid FROM revs " + + "WHERE doc_id=? and revid in (" + quotedRevIds + + ") and revid <= ? " + "ORDER BY revid DESC LIMIT 1"; + String[] args = { Long.toString(docNumericID) }; + + Cursor cursor = null; + try { + cursor = database.rawQuery(sql, args); + cursor.moveToFirst(); + if (!cursor.isAfterLast()) { + result = cursor.getString(0); + } + + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting all revisions of document", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + /** + * Returns an array of TDRevs in reverse chronological order, starting with + * the given revision. + */ + public List getRevisionHistory(TDRevision rev) { + String docId = rev.getDocId(); + String revId = rev.getRevId(); + assert ((docId != null) && (revId != null)); + + long docNumericId = getDocNumericID(docId); + if (docNumericId < 0) { + return null; + } else if (docNumericId == 0) { + return new ArrayList(); + } + + String sql = "SELECT sequence, parent, revid, deleted FROM revs " + + "WHERE doc_id=? ORDER BY sequence DESC"; + String[] args = { Long.toString(docNumericId) }; + Cursor cursor = null; + + List result; + try { + cursor = database.rawQuery(sql, args); + + cursor.moveToFirst(); + long lastSequence = 0; + result = new ArrayList(); + while (!cursor.isAfterLast()) { + long sequence = cursor.getLong(0); + boolean matches = false; + if (lastSequence == 0) { + matches = revId.equals(cursor.getString(2)); + } else { + matches = (sequence == lastSequence); + } + if (matches) { + revId = cursor.getString(2); + boolean deleted = (cursor.getInt(3) > 0); + TDRevision aRev = new TDRevision(docId, revId, deleted); + aRev.setSequence(cursor.getLong(0)); + result.add(aRev); + lastSequence = cursor.getLong(1); + if (lastSequence == 0) { + break; + } + } + cursor.moveToNext(); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting revision history", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + // Splits a revision ID into its generation number and opaque suffix string + public static int parseRevIDNumber(String rev) { + int result = -1; + int dashPos = rev.indexOf("-"); + if (dashPos >= 0) { + try { + result = Integer.parseInt(rev.substring(0, dashPos)); + } catch (NumberFormatException e) { + // ignore, let it return -1 + } + } + return result; + } + + // Splits a revision ID into its generation number and opaque suffix string + public static String parseRevIDSuffix(String rev) { + String result = null; + int dashPos = rev.indexOf("-"); + if (dashPos >= 0) { + result = rev.substring(dashPos + 1); + } + return result; + } + + public static Map makeRevisionHistoryDict( + List history) { + if (history == null) { + return null; + } + + // Try to extract descending numeric prefixes: + List suffixes = new ArrayList(); + int start = -1; + int lastRevNo = -1; + for (TDRevision rev : history) { + int revNo = parseRevIDNumber(rev.getRevId()); + String suffix = parseRevIDSuffix(rev.getRevId()); + if (revNo > 0 && suffix.length() > 0) { + if (start < 0) { + start = revNo; + } else if (revNo != lastRevNo - 1) { + start = -1; + break; + } + lastRevNo = revNo; + suffixes.add(suffix); + } else { + start = -1; + break; + } + } + + Map result = new HashMap(); + if (start == -1) { + // we failed to build sequence, just stuff all the revs in list + suffixes = new ArrayList(); + for (TDRevision rev : history) { + suffixes.add(rev.getRevId()); + } + } else { + result.put("start", start); + } + result.put("ids", suffixes); + + return result; + } + + /** + * Returns the revision history as a _revisions dictionary, as returned by + * the REST API's ?revs=true option. + */ + public Map getRevisionHistoryDict(TDRevision rev) { + return makeRevisionHistoryDict(getRevisionHistory(rev)); + } + + public TDRevisionList changesSince(long lastSeq, TDChangesOptions options, + TDFilterBlock filter) { + // http://wiki.apache.org/couchdb/HTTP_database_API#Changes + if (options == null) { + options = new TDChangesOptions(); + } + + boolean includeDocs = options.isIncludeDocs() || (filter != null); + String additionalSelectColumns = ""; + if (includeDocs) { + additionalSelectColumns = ", json"; + } + + String sql = "SELECT sequence, revs.doc_id, docid, revid, deleted" + + additionalSelectColumns + " FROM revs, docs " + + "WHERE sequence > ? AND current=1 " + + "AND revs.doc_id = docs.doc_id " + + "ORDER BY revs.doc_id, revid DESC"; + String[] args = { Long.toString(lastSeq) }; + Cursor cursor = null; + TDRevisionList changes = null; + + try { + cursor = database.rawQuery(sql, args); + cursor.moveToFirst(); + changes = new TDRevisionList(); + long lastDocId = 0; + while (!cursor.isAfterLast()) { + if (!options.isIncludeConflicts()) { + // Only count the first rev for a given doc (the rest will + // be losing conflicts): + long docNumericId = cursor.getLong(1); + if (docNumericId == lastDocId) { + cursor.moveToNext(); + continue; + } + lastDocId = docNumericId; + } + + TDRevision rev = new TDRevision(cursor.getString(2), + cursor.getString(3), (cursor.getInt(4) > 0)); + rev.setSequence(cursor.getLong(0)); + if (includeDocs) { + expandStoredJSONIntoRevisionWithAttachments( + cursor.getBlob(5), rev, options.getContentOptions()); + } + if ((filter == null) || (filter.filter(rev))) { + changes.add(rev); + } + cursor.moveToNext(); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error looking for changes", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + if (options.isSortBySequence()) { + changes.sortBySequence(); + } + changes.limit(options.getLimit()); + return changes; + } + + /** + * Define or clear a named filter function. + * + * These aren't used directly by TDDatabase, but they're looked up by + * TDRouter when a _changes request has a ?filter parameter. + */ + public void defineFilter(String filterName, TDFilterBlock filter) { + if (filters == null) { + filters = new HashMap(); + } + filters.put(filterName, filter); + } + + public TDFilterBlock getFilterNamed(String filterName) { + TDFilterBlock result = null; + if (filters != null) { + result = filters.get(filterName); + } + return result; + } + + /** VIEWS: **/ + + public TDView registerView(TDView view) { + if (view == null) { + return null; + } + if (views == null) { + views = new HashMap(); + } + views.put(view.getName(), view); + return view; + } + + public TDView getViewNamed(String name) { + TDView view = null; + if (views != null) { + view = views.get(name); + } + if (view != null) { + return view; + } + return registerView(new TDView(this, name)); + } + + public TDView getExistingViewNamed(String name) { + TDView view = null; + if (views != null) { + view = views.get(name); + } + if (view != null) { + return view; + } + view = new TDView(this, name); + if (view.getViewId() == 0) { + return null; + } + + return registerView(view); + } + + public List getAllViews() { + Cursor cursor = null; + List result = null; + + try { + cursor = database.rawQuery("SELECT name FROM views", null); + cursor.moveToFirst(); + result = new ArrayList(); + while (!cursor.isAfterLast()) { + result.add(getViewNamed(cursor.getString(0))); + cursor.moveToNext(); + } + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error getting all views", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + public TDStatus deleteViewNamed(String name) { + TDStatus result = new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + try { + String[] whereArgs = { name }; + int rowsAffected = database.delete("views", "name=?", whereArgs); + if (rowsAffected > 0) { + result.setCode(TDStatus.OK); + } else { + result.setCode(TDStatus.NOT_FOUND); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error deleting view", e); + } + return result; + } + + // FIX: This has a lot of code in common with -[TDView + // queryWithOptions:status:]. Unify the two! + public Map getDocsWithIDs(List docIDs, + TDQueryOptions options) { + if (options == null) { + options = new TDQueryOptions(); + } + + long updateSeq = 0; + if (options.isUpdateSeq()) { + updateSeq = getLastSequence(); // TODO: needs to be atomic with the + // following SELECT + } + + // Generate the SELECT statement, based on the options: + String additionalCols = ""; + if (options.isIncludeDocs()) { + additionalCols = ", json, sequence"; + } + String sql = "SELECT revs.doc_id, docid, revid, deleted" + + additionalCols + " FROM revs, docs WHERE"; + + if (docIDs != null) { + sql += " docid IN (" + joinQuoted(docIDs) + ")"; + } else { + sql += " deleted=0"; + } + + sql += " AND current=1 AND docs.doc_id = revs.doc_id"; + + List argsList = new ArrayList(); + Object minKey = options.getStartKey(); + Object maxKey = options.getEndKey(); + boolean inclusiveMin = true; + boolean inclusiveMax = options.isInclusiveEnd(); + if (options.isDescending()) { + minKey = maxKey; + maxKey = options.getStartKey(); + inclusiveMin = inclusiveMax; + inclusiveMax = true; + } + + if (minKey != null) { + assert (minKey instanceof String); + if (inclusiveMin) { + sql += " AND docid >= ?"; + } else { + sql += " AND docid > ?"; + } + argsList.add((String) minKey); + } + + if (maxKey != null) { + assert (maxKey instanceof String); + if (inclusiveMax) { + sql += " AND docid <= ?"; + } else { + sql += " AND docid < ?"; + } + argsList.add((String) maxKey); + } + + String order = "ASC"; + if (options.isDescending()) { + order = "DESC"; + } + + sql += " ORDER BY docid " + order + ", revid DESC LIMIT ? OFFSET ?"; + + argsList.add(Integer.toString(options.getLimit())); + argsList.add(Integer.toString(options.getSkip())); + Cursor cursor = null; + long lastDocID = 0; + List> rows = null; + + try { + cursor = database.rawQuery(sql, + argsList.toArray(new String[argsList.size()])); + + cursor.moveToFirst(); + rows = new ArrayList>(); + while (!cursor.isAfterLast()) { + long docNumericID = cursor.getLong(0); + if (docNumericID == lastDocID) { + cursor.moveToNext(); + continue; + } + lastDocID = docNumericID; + + String docId = cursor.getString(1); + String revId = cursor.getString(2); + Map docContents = null; + boolean deleted = cursor.getInt(3) > 0; + if (options.isIncludeDocs() && !deleted) { + byte[] json = cursor.getBlob(4); + long sequence = cursor.getLong(5); + docContents = documentPropertiesFromJSON(json, docId, + revId, sequence, options.getContentOptions()); + } + + Map valueMap = new HashMap(); + valueMap.put("rev", revId); + + Map change = new HashMap(); + change.put("id", docId); + change.put("key", docId); + change.put("value", valueMap); + if (docContents != null) { + change.put("doc", docContents); + } + if (deleted) { + change.put("deleted", true); + } + + rows.add(change); + + cursor.moveToNext(); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting all docs", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + + int totalRows = cursor.getCount(); // ??? Is this true, or does it + // ignore limit/offset? + Map result = new HashMap(); + result.put("rows", rows); + result.put("total_rows", totalRows); + result.put("offset", options.getSkip()); + if (updateSeq != 0) { + result.put("update_seq", updateSeq); + } + + return result; + } + + public Map getAllDocs(TDQueryOptions options) { + return getDocsWithIDs(null, options); + } + + /*************************************************************************************************/ + /*** TDDatabase+Attachments ***/ + /*************************************************************************************************/ + + public TDStatus insertAttachmentForSequenceWithNameAndType( + InputStream contentStream, long sequence, String name, + String contentType, int revpos) { + assert (sequence > 0); + assert (name != null); + + TDBlobKey key = new TDBlobKey(); + if (!attachments.storeBlobStream(contentStream, key)) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + byte[] keyData = key.getBytes(); + try { + ContentValues args = new ContentValues(); + args.put("sequence", sequence); + args.put("filename", name); + args.put("key", keyData); + args.put("type", contentType); + args.put("length", attachments.getSizeOfBlob(key)); + args.put("revpos", revpos); + database.insert("attachments", null, args); + return new TDStatus(TDStatus.CREATED); + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error inserting attachment", e); + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + } + + public TDStatus copyAttachmentNamedFromSequenceToSequence(String name, + long fromSeq, long toSeq) { + assert (name != null); + assert (toSeq > 0); + if (fromSeq < 0) { + return new TDStatus(TDStatus.NOT_FOUND); + } + + Cursor cursor = null; + + String[] args = { Long.toString(toSeq), name, Long.toString(fromSeq), + name }; + try { + database.execSQL( + "INSERT INTO attachments (sequence, filename, key, type, length, revpos) " + + "SELECT ?, ?, key, type, length, revpos FROM attachments " + + "WHERE sequence=? AND filename=?", args); + cursor = database.rawQuery("SELECT changes()", null); + cursor.moveToFirst(); + int rowsUpdated = cursor.getInt(0); + if (rowsUpdated == 0) { + // Oops. This means a glitch in our attachment-management or + // pull code, + // or else a bug in the upstream server. + Log.w(TDDatabase.TAG, "Can't find inherited attachment " + name + + " from seq# " + Long.toString(fromSeq) + + " to copy to " + Long.toString(toSeq)); + return new TDStatus(TDStatus.NOT_FOUND); + } else { + return new TDStatus(TDStatus.OK); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error copying attachment", e); + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Returns the content and MIME type of an attachment + */ + public TDAttachment getAttachmentForSequence(long sequence, + String filename, TDStatus status) { + assert (sequence > 0); + assert (filename != null); + + Cursor cursor = null; + + String[] args = { Long.toString(sequence), filename }; + try { + cursor = database + .rawQuery( + "SELECT key, type FROM attachments WHERE sequence=? AND filename=?", + args); + + if (!cursor.moveToFirst()) { + status.setCode(TDStatus.NOT_FOUND); + return null; + } + + byte[] keyData = cursor.getBlob(0); + // TODO add checks on key here? (ios version) + TDBlobKey key = new TDBlobKey(keyData); + InputStream contentStream = attachments.blobStreamForKey(key); + if (contentStream == null) { + Log.e(TDDatabase.TAG, "Failed to load attachment"); + status.setCode(TDStatus.INTERNAL_SERVER_ERROR); + return null; + } else { + status.setCode(TDStatus.OK); + TDAttachment result = new TDAttachment(); + result.setContentStream(contentStream); + result.setContentType(cursor.getString(1)); + return result; + } + + } catch (SQLException e) { + status.setCode(TDStatus.INTERNAL_SERVER_ERROR); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + + } + + /** + * Constructs an "_attachments" dictionary for a revision, to be inserted in + * its JSON body. + */ + public Map getAttachmentsDictForSequenceWithContent( + long sequence, boolean withContent) { + assert (sequence > 0); + + Cursor cursor = null; + + String args[] = { Long.toString(sequence) }; + try { + cursor = database + .rawQuery( + "SELECT filename, key, type, length, revpos FROM attachments WHERE sequence=?", + args); + + if (!cursor.moveToFirst()) { + return null; + } + + Map result = new HashMap(); + + while (!cursor.isAfterLast()) { + + byte[] keyData = cursor.getBlob(1); + TDBlobKey key = new TDBlobKey(keyData); + String digestString = "sha1-" + Base64.encodeBytes(keyData); + String dataBase64 = null; + if (withContent) { + byte[] data = attachments.blobForKey(key); + if (data != null) { + dataBase64 = Base64.encodeBytes(data); + } else { + Log.w(TDDatabase.TAG, "Error loading attachment"); + } + } + + Map attachment = new HashMap(); + if (dataBase64 == null) { + attachment.put("stub", true); + } else { + attachment.put("data", dataBase64); + } + attachment.put("digest", digestString); + attachment.put("content_type", cursor.getString(2)); + attachment.put("length", cursor.getInt(3)); + attachment.put("revpos", cursor.getInt(4)); + + result.put(cursor.getString(0), attachment); + + cursor.moveToNext(); + } + + return result; + + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting attachments for sequence", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Modifies a TDRevision's body by changing all attachments with revpos < + * minRevPos into stubs. + * + * @param rev + * @param minRevPos + */ + public void stubOutAttachmentsIn(TDRevision rev, int minRevPos) { + if (minRevPos <= 1) { + return; + } + Map properties = (Map) rev + .getProperties(); + Map attachments = null; + if (properties != null) { + attachments = (Map) properties.get("_attachments"); + } + Map editedProperties = null; + Map editedAttachments = null; + for (String name : attachments.keySet()) { + Map attachment = (Map) attachments + .get(name); + int revPos = (Integer) attachment.get("revpos"); + Object stub = attachment.get("stub"); + if (revPos > 0 && revPos < minRevPos && (stub == null)) { + // Strip this attachment's body. First make its dictionary + // mutable: + if (editedProperties == null) { + editedProperties = new HashMap(properties); + editedAttachments = new HashMap(attachments); + editedProperties.put("_attachments", editedAttachments); + } + // ...then remove the 'data' and 'follows' key: + Map editedAttachment = new HashMap( + attachment); + editedAttachment.remove("data"); + editedAttachment.remove("follows"); + editedAttachment.put("stub", true); + editedAttachments.put(name, editedAttachment); + Log.d(TDDatabase.TAG, "Stubbed out attachment" + rev + " " + + name + ": revpos" + revPos + " " + minRevPos); + } + } + if (editedProperties != null) + rev.setProperties(editedProperties); + } + + /** + * Given a newly-added revision, adds the necessary attachment rows to the + * database and stores inline attachments into the blob store. + */ + public TDStatus processAttachmentsForRevision(TDRevision rev, + long parentSequence) { + assert (rev != null); + long newSequence = rev.getSequence(); + assert (newSequence > parentSequence); + + // If there are no attachments in the new rev, there's nothing to do: + Map newAttachments = null; + Map properties = (Map) rev + .getProperties(); + if (properties != null) { + newAttachments = (Map) properties + .get("_attachments"); + } + if (newAttachments == null || newAttachments.size() == 0 + || rev.isDeleted()) { + return new TDStatus(TDStatus.OK); + } + + for (String name : newAttachments.keySet()) { + + TDStatus status = new TDStatus(); + Map newAttach = (Map) newAttachments + .get(name); + String newContentBase64 = (String) newAttach.get("data"); + if (newContentBase64 != null) { + // New item contains data, so insert it. First decode the data: + byte[] newContents; + try { + newContents = Base64.decode(newContentBase64); + } catch (IOException e) { + Log.e(TDDatabase.TAG, "IOExeption parsing base64", e); + return new TDStatus(TDStatus.BAD_REQUEST); + } + if (newContents == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + // Now determine the revpos, i.e. generation # this was added + // in. Usually this is + // implicit, but a rev being pulled in replication will have it + // set already. + int generation = rev.getGeneration(); + assert (generation > 0); + Object revposObj = newAttach.get("revpos"); + int revpos = generation; + if (revposObj != null && revposObj instanceof Integer) { + revpos = ((Integer) revposObj).intValue(); + } + + if (revpos > generation) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + // Finally insert the attachment: + status = insertAttachmentForSequenceWithNameAndType( + new ByteArrayInputStream(newContents), newSequence, + name, (String) newAttach.get("content_type"), revpos); + } else { + // It's just a stub, so copy the previous revision's attachment + // entry: + // ? Should I enforce that the type and digest (if any) match? + status = copyAttachmentNamedFromSequenceToSequence(name, + parentSequence, newSequence); + } + if (!status.isSuccessful()) { + return status; + } + } + + return new TDStatus(TDStatus.OK); + } + + /** + * Updates or deletes an attachment, creating a new document revision in the + * process. Used by the PUT / DELETE methods called on attachment URLs. + */ + public TDRevision updateAttachment(String filename, + InputStream contentStream, String contentType, String docID, + String oldRevID, TDStatus status) { + status.setCode(TDStatus.BAD_REQUEST); + if (filename == null || filename.length() == 0 + || (contentStream != null && contentType == null) + || (oldRevID != null && docID == null) + || (contentStream != null && docID == null)) { + return null; + } + + beginTransaction(); + try { + TDRevision oldRev = new TDRevision(docID, oldRevID, false); + if (oldRevID != null) { + // Load existing revision if this is a replacement: + TDStatus loadStatus = loadRevisionBody(oldRev, + EnumSet.noneOf(TDContentOptions.class)); + status.setCode(loadStatus.getCode()); + if (!status.isSuccessful()) { + if (status.getCode() == TDStatus.NOT_FOUND + && existsDocumentWithIDAndRev(docID, null)) { + status.setCode(TDStatus.CONFLICT); // if some other + // revision exists, + // it's a conflict + } + return null; + } + + Map attachments = (Map) oldRev + .getProperties().get("_attachments"); + if (contentStream == null && attachments != null + && !attachments.containsKey(filename)) { + status.setCode(TDStatus.NOT_FOUND); + return null; + } + // Remove the _attachments stubs so putRevision: doesn't copy + // the rows for me + // OPT: Would be better if I could tell loadRevisionBody: not to + // add it + if (attachments != null) { + Map properties = new HashMap( + oldRev.getProperties()); + properties.remove("_attachments"); + oldRev.setBody(new TDBody(properties)); + } + } else { + // If this creates a new doc, it needs a body: + oldRev.setBody(new TDBody(new HashMap())); + } + + // Create a new revision: + TDRevision newRev = putRevision(oldRev, oldRevID, false, status); + if (newRev == null) { + return null; + } + + if (oldRevID != null) { + // Copy all attachment rows _except_ for the one being updated: + String[] args = { Long.toString(newRev.getSequence()), + Long.toString(oldRev.getSequence()), filename }; + database.execSQL( + "INSERT INTO attachments " + + "(sequence, filename, key, type, length, revpos) " + + "SELECT ?, filename, key, type, length, revpos FROM attachments " + + "WHERE sequence=? AND filename != ?", args); + } + + if (contentStream != null) { + // If not deleting, add a new attachment entry: + TDStatus insertStatus = insertAttachmentForSequenceWithNameAndType( + contentStream, newRev.getSequence(), filename, + contentType, newRev.getGeneration()); + status.setCode(insertStatus.getCode()); + + if (!status.isSuccessful()) { + return null; + } + } + + status.setCode((contentStream != null) ? TDStatus.CREATED + : TDStatus.OK); + return newRev; + + } catch (SQLException e) { + Log.e(TAG, "Error uploading attachment", e); + status.setCode(TDStatus.INTERNAL_SERVER_ERROR); + return null; + } finally { + endTransaction(status.isSuccessful()); + } + } + + /** + * Deletes obsolete attachments from the database and blob store. + */ + public TDStatus garbageCollectAttachments() { + // First delete attachment rows for already-cleared revisions: + // OPT: Could start after last sequence# we GC'd up to + + try { + database.execSQL("DELETE FROM attachments WHERE sequence IN " + + "(SELECT sequence from revs WHERE json IS null)"); + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error deleting attachments", e); + } + + // Now collect all remaining attachment IDs and tell the store to delete + // all but these: + Cursor cursor = null; + try { + cursor = database.rawQuery("SELECT DISTINCT key FROM attachments", + null); + + cursor.moveToFirst(); + List allKeys = new ArrayList(); + while (!cursor.isAfterLast()) { + TDBlobKey key = new TDBlobKey(cursor.getBlob(0)); + allKeys.add(key); + cursor.moveToNext(); + } + + int numDeleted = attachments.deleteBlobsExceptWithKeys(allKeys); + if (numDeleted < 0) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + Log.v(TDDatabase.TAG, "Deleted " + numDeleted + " attachments"); + + return new TDStatus(TDStatus.OK); + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error finding attachment keys in use", e); + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /*************************************************************************************************/ + /*** TDDatabase+Insertion ***/ + /*************************************************************************************************/ + + /** DOCUMENT & REV IDS: **/ + + public static boolean isValidDocumentId(String id) { + // http://wiki.apache.org/couchdb/HTTP_Document_API#Documents + if (id == null || id.length() == 0) { + return false; + } + if (id.charAt(0) == '_') { + return (id.startsWith("_design/")); + } + return true; + // "_local/*" is not a valid document ID. Local docs have their own API + // and shouldn't get here. + } + + public static String generateDocumentId() { + return TDMisc.TDCreateUUID(); + } + + public String generateNextRevisionID(String revisionId) { + // Revision IDs have a generation count, a hyphen, and a UUID. + int generation = 0; + if (revisionId != null) { + generation = TDRevision.generationFromRevID(revisionId); + if (generation == 0) { + return null; + } + } + String digest = TDMisc.TDCreateUUID(); // TODO: Generate canonical + // digest of body + return Integer.toString(generation + 1) + "-" + digest; + } + + public long insertDocumentID(String docId) { + long rowId = -1; + try { + ContentValues args = new ContentValues(); + args.put("docid", docId); + rowId = database.insert("docs", null, args); + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error inserting document id", e); + } + return rowId; + } + + public long getOrInsertDocNumericID(String docId) { + long docNumericId = getDocNumericID(docId); + if (docNumericId == 0) { + docNumericId = insertDocumentID(docId); + } + return docNumericId; + } + + /** + * Parses the _revisions dict from a document into an array of revision ID + * strings + */ + public static List parseCouchDBRevisionHistory( + Map docProperties) { + Map revisions = (Map) docProperties + .get("_revisions"); + if (revisions == null) { + return null; + } + List revIDs = (List) revisions.get("ids"); + Integer start = (Integer) revisions.get("start"); + if (start != null) { + for (int i = 0; i < revIDs.size(); i++) { + String revID = revIDs.get(i); + revIDs.set(i, Integer.toString(start--) + "-" + revID); + } + } + return revIDs; + } + + /** INSERTION: **/ + + public byte[] encodeDocumentJSON(TDRevision rev) { + + Map origProps = rev.getProperties(); + if (origProps == null) { + return null; + } + + // Don't allow any "_"-prefixed keys. Known ones we'll ignore, unknown + // ones are an error. + Map properties = new HashMap( + origProps.size()); + for (String key : origProps.keySet()) { + if (key.startsWith("_")) { + if (!KNOWN_SPECIAL_KEYS.contains(key)) { + Log.e(TAG, "TDDatabase: Invalid top-level key '" + key + + "' in document to be inserted"); + return null; + } + } else { + properties.put(key, origProps.get(key)); + } + } + + byte[] json = null; + try { + json = TDServer.getObjectMapper().writeValueAsBytes(properties); + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error serializing " + rev + " to JSON", e); + } + return json; + } + + public void notifyChange(TDRevision rev, URL source) { + Map changeNotification = new HashMap(); + changeNotification.put("rev", rev); + changeNotification.put("seq", rev.getSequence()); + if (source != null) { + changeNotification.put("source", source); + } + setChanged(); + notifyObservers(changeNotification); + } + + public long insertRevision(TDRevision rev, long docNumericID, + long parentSequence, boolean current, byte[] data) { + long rowId = 0; + try { + ContentValues args = new ContentValues(); + args.put("doc_id", docNumericID); + args.put("revid", rev.getRevId()); + if (parentSequence != 0) { + args.put("parent", parentSequence); + } + args.put("current", current); + args.put("deleted", rev.isDeleted()); + args.put("json", data); + rowId = database.insert("revs", null, args); + rev.setSequence(rowId); + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error inserting revision", e); + } + return rowId; + } + + private TDRevision putRevision(TDRevision rev, String prevRevId, + TDStatus resultStatus) { + return putRevision(rev, prevRevId, false, resultStatus); + } + + /** + * Stores a new (or initial) revision of a document. + * + * This is what's invoked by a PUT or POST. As with those, the previous + * revision ID must be supplied when necessary and the call will fail if it + * doesn't match. + * + * @param rev + * The revision to add. If the docID is null, a new UUID will be + * assigned. Its revID must be null. It must have a JSON body. + * @param prevRevId + * The ID of the revision to replace (same as the "?rev=" + * parameter to a PUT), or null if this is a new document. + * @param allowConflict + * If false, an error status 409 will be returned if the + * insertion would create a conflict, i.e. if the previous + * revision already has a child. + * @param resultStatus + * On return, an HTTP status code indicating success or failure. + * @return A new TDRevision with the docID, revID and sequence filled in + * (but no body). + */ + @SuppressWarnings("unchecked") + public TDRevision putRevision(TDRevision rev, String prevRevId, + boolean allowConflict, TDStatus resultStatus) { + // prevRevId is the rev ID being replaced, or nil if an insert + String docId = rev.getDocId(); + boolean deleted = rev.isDeleted(); + if ((rev == null) || ((prevRevId != null) && (docId == null)) + || (deleted && (docId == null)) + || ((docId != null) && !isValidDocumentId(docId))) { + resultStatus.setCode(TDStatus.BAD_REQUEST); + return null; + } + + resultStatus.setCode(TDStatus.INTERNAL_SERVER_ERROR); + beginTransaction(); + Cursor cursor = null; + + // // PART I: In which are performed lookups and validations prior to + // the insert... + + long docNumericID = (docId != null) ? getDocNumericID(docId) : 0; + long parentSequence = 0; + try { + if (prevRevId != null) { + // Replacing: make sure given prevRevID is current & find its + // sequence number: + if (docNumericID <= 0) { + resultStatus.setCode(TDStatus.NOT_FOUND); + return null; + } + + String[] args = { Long.toString(docNumericID), prevRevId }; + String additionalWhereClause = ""; + if (!allowConflict) { + additionalWhereClause = "AND current=1"; + } + + cursor = database.rawQuery( + "SELECT sequence FROM revs WHERE doc_id=? AND revid=? " + + additionalWhereClause + " LIMIT 1", args); + + if (cursor.moveToFirst()) { + parentSequence = cursor.getLong(0); + } + + if (parentSequence == 0) { + // Not found: either a 404 or a 409, depending on whether + // there is any current revision + if (!allowConflict + && existsDocumentWithIDAndRev(docId, null)) { + resultStatus.setCode(TDStatus.CONFLICT); + return null; + } else { + resultStatus.setCode(TDStatus.NOT_FOUND); + return null; + } + } + + if (validations != null && validations.size() > 0) { + // Fetch the previous revision and validate the new one + // against it: + TDRevision prevRev = new TDRevision(docId, prevRevId, false); + TDStatus status = validateRevision(rev, prevRev); + if (!status.isSuccessful()) { + resultStatus.setCode(status.getCode()); + return null; + } + } + + // Make replaced rev non-current: + ContentValues updateContent = new ContentValues(); + updateContent.put("current", 0); + database.update("revs", updateContent, "sequence=" + + parentSequence, null); + } else { + // Inserting first revision. + if (deleted && (docId != null)) { + // Didn't specify a revision to delete: 404 or a 409, + // depending + if (existsDocumentWithIDAndRev(docId, null)) { + resultStatus.setCode(TDStatus.CONFLICT); + return null; + } else { + resultStatus.setCode(TDStatus.NOT_FOUND); + return null; + } + } + + // Validate: + TDStatus status = validateRevision(rev, null); + if (!status.isSuccessful()) { + resultStatus.setCode(status.getCode()); + return null; + } + + if (docId != null) { + // Inserting first revision, with docID given (PUT): + if (docNumericID <= 0) { + // Doc doesn't exist at all; create it: + docNumericID = insertDocumentID(docId); + if (docNumericID <= 0) { + return null; + } + } else { + // Doc exists; check whether current winning revision is + // deleted: + String[] args = { Long.toString(docNumericID) }; + cursor = database + .rawQuery( + "SELECT sequence, deleted FROM revs WHERE doc_id=? and current=1 ORDER BY revid DESC LIMIT 1", + args); + + if (cursor.moveToFirst()) { + boolean wasAlreadyDeleted = (cursor.getInt(1) > 0); + if (wasAlreadyDeleted) { + // Make the deleted revision no longer current: + ContentValues updateContent = new ContentValues(); + updateContent.put("current", 0); + database.update("revs", updateContent, + "sequence=" + cursor.getLong(0), null); + } else if (!allowConflict) { + // docId already exists, current not deleted, + // conflict + resultStatus.setCode(TDStatus.CONFLICT); + return null; + } + } + } + } else { + // Inserting first revision, with no docID given (POST): + // generate a unique docID: + docId = TDDatabase.generateDocumentId(); + docNumericID = insertDocumentID(docId); + if (docNumericID <= 0) { + return null; + } + } + } + + // // PART II: In which insertion occurs... + + // Bump the revID and update the JSON: + String newRevId = generateNextRevisionID(prevRevId); + byte[] data = null; + if (!rev.isDeleted()) { + data = encodeDocumentJSON(rev); + if (data == null) { + // bad or missing json + resultStatus.setCode(TDStatus.BAD_REQUEST); + return null; + } + } + + rev = rev.copyWithDocID(docId, newRevId); + + // Now insert the rev itself: + long newSequence = insertRevision(rev, docNumericID, + parentSequence, true, data); + if (newSequence == 0) { + return null; + } + + // Store any attachments: + if (attachments != null) { + TDStatus status = processAttachmentsForRevision(rev, + parentSequence); + if (!status.isSuccessful()) { + resultStatus.setCode(status.getCode()); + return null; + } + } + + // Success! + if (deleted) { + resultStatus.setCode(TDStatus.OK); + } else { + resultStatus.setCode(TDStatus.CREATED); + } + + } catch (SQLException e1) { + Log.e(TDDatabase.TAG, "Error putting revision", e1); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + endTransaction(resultStatus.isSuccessful()); + } + + // // EPILOGUE: A change notification is sent... + notifyChange(rev, null); + return rev; + } + + /** + * Inserts an already-existing revision replicated from a remote database. + * + * It must already have a revision ID. This may create a conflict! The + * revision's history must be given; ancestor revision IDs that don't + * already exist locally will create phantom revisions with no content. + */ + public TDStatus forceInsert(TDRevision rev, List revHistory, + URL source) { + + String docId = rev.getDocId(); + String revId = rev.getRevId(); + if (!isValidDocumentId(docId) || (revId == null)) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + int historyCount = revHistory.size(); + if (historyCount == 0) { + revHistory = new ArrayList(); + revHistory.add(revId); + historyCount = 1; + } else if (!revHistory.get(0).equals(rev.getRevId())) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + boolean success = false; + beginTransaction(); + try { + // First look up all locally-known revisions of this document: + long docNumericID = getOrInsertDocNumericID(docId); + TDRevisionList localRevs = getAllRevisionsOfDocumentID(docId, + docNumericID, false); + if (localRevs == null) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + // Walk through the remote history in chronological order, matching + // each revision ID to + // a local revision. When the list diverges, start creating blank + // local revisions to fill + // in the local history: + long sequence = 0; + long localParentSequence = 0; + for (int i = revHistory.size() - 1; i >= 0; --i) { + revId = revHistory.get(i); + TDRevision localRev = localRevs.revWithDocIdAndRevId(docId, + revId); + if (localRev != null) { + // This revision is known locally. Remember its sequence as + // the parent of the next one: + sequence = localRev.getSequence(); + assert (sequence > 0); + localParentSequence = sequence; + } else { + // This revision isn't known, so add it: + TDRevision newRev; + byte[] data = null; + boolean current = false; + if (i == 0) { + // Hey, this is the leaf revision we're inserting: + newRev = rev; + if (!rev.isDeleted()) { + data = encodeDocumentJSON(rev); + if (data == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + } + current = true; + } else { + // It's an intermediate parent, so insert a stub: + newRev = new TDRevision(docId, revId, false); + } + + // Insert it: + sequence = insertRevision(newRev, docNumericID, sequence, + current, data); + + if (sequence <= 0) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + if (i == 0) { + // Write any changed attachments for the new revision: + TDStatus status = processAttachmentsForRevision(rev, + localParentSequence); + if (!status.isSuccessful()) { + return status; + } + } + } + } + + // Mark the latest local rev as no longer current: + if (localParentSequence > 0 && localParentSequence != sequence) { + ContentValues args = new ContentValues(); + args.put("current", 0); + String[] whereArgs = { Long.toString(localParentSequence) }; + try { + database.update("revs", args, "sequence=?", whereArgs); + } catch (SQLException e) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + } + + success = true; + } catch (SQLException e) { + endTransaction(success); + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } finally { + endTransaction(success); + } + + // Notify and return: + notifyChange(rev, source); + return new TDStatus(TDStatus.CREATED); + } + + /** VALIDATION **/ + + /** + * Define or clear a named document validation function. + */ + public void defineValidation(String name, TDValidationBlock validationBlock) { + if (validations == null) { + validations = new HashMap(); + } + validations.put(name, validationBlock); + } + + public TDValidationBlock getValidationNamed(String name) { + TDValidationBlock result = null; + if (validations != null) { + result = validations.get(name); + } + return result; + } + + public TDStatus validateRevision(TDRevision newRev, TDRevision oldRev) { + TDStatus result = new TDStatus(TDStatus.OK); + if (validations == null || validations.size() == 0) { + return result; + } + TDValidationContextImpl context = new TDValidationContextImpl(this, + oldRev); + for (String validationName : validations.keySet()) { + TDValidationBlock validation = getValidationNamed(validationName); + if (!validation.validate(newRev, context)) { + result.setCode(context.getErrorType().getCode()); + break; + } + } + return result; + } + + /*************************************************************************************************/ + /*** TDDatabase+Replication ***/ + /*************************************************************************************************/ + + // TODO implement missing replication methods + + public List getActiveReplicators() { + return activeReplicators; + } + + public TDReplicator getActiveReplicator(URL remote, boolean push) { + if (activeReplicators != null) { + for (TDReplicator replicator : activeReplicators) { + if (replicator.getRemote().equals(remote) + && replicator.isPush() == push + && replicator.isRunning()) { + return replicator; + } + } + } + return null; + } + + public TDReplicator getReplicator(URL remote, boolean push, + String access_token, boolean continuous, + ScheduledExecutorService workExecutor) { + TDReplicator replicator = getReplicator(remote, null, push, + access_token, continuous, workExecutor); + return replicator; + } + + public TDReplicator getReplicator(URL remote, + HttpClientFactory httpClientFactory, boolean push, + String access_token, boolean continuous, + ScheduledExecutorService workExecutor) { + TDReplicator result = getActiveReplicator(remote, push); + if (result != null) { + return result; + } + result = push ? new TDPusher(this, remote, access_token, continuous, + httpClientFactory, workExecutor) : new TDPuller(this, remote, + access_token, continuous, httpClientFactory, workExecutor); + + if (activeReplicators == null) { + activeReplicators = new ArrayList(); + } + activeReplicators.add(result); + return result; + } + + public TDRevisionList getPendingRevisions(URL url, boolean push, + long lastUpdated) { + Log.d("ARTOOPULLER", String.format("Called with %d", lastUpdated)); + TDRevisionList result = new TDRevisionList(); + String[] args = { + url.toExternalForm(), + Integer.toString(push ? 1 : 0), + "" + + ((lastUpdated == -1 ? new Date().getTime() + : lastUpdated) - (60 * 1000)) }; + Cursor cursor = database + .rawQuery( + "SELECT docid, revid, deleted, sequence, lastUpdated FROM replicator_log WHERE (remote=? AND push=?) AND (lastUpdated IS NULL OR lastUpdated < ?) LIMIT 100", + args); + if (cursor.moveToFirst()) { + do { + TDRevision rev = new TDRevision(cursor.getString(0), + cursor.getString(1), cursor.getInt(2) == 1 ? true + : false); + rev.setSequence(cursor.getLong(3)); + result.add(rev); + + // Log.d("ARTOOPULLER", String.format( + // "lastUpdated:%d; for doc %s, %d. comparision %d sec", + // lastUpdated, cursor.getString(0), cursor.getLong(4), + // (lastUpdated - cursor.getLong(4)) / 1000)); + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return result; + } + + public boolean logRevision(URL url, boolean push, TDRevision rev) { + boolean success = false; + Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), + rev.getDocId(), rev.getRevId(), (rev.isDeleted() ? 1 : 0), + rev.getSequence() }; + Cursor cursor = null; + try { + database.execSQL( + "INSERT INTO replicator_log(remote, push, docid, revid, deleted, sequence, lastUpdated) VALUES(?,?,?,?,?,?,0)", + args); + cursor = database.rawQuery("SELECT changes()", null); + if (cursor.moveToFirst() && cursor.getInt(0) > 0) { + success = true; + } + } catch (SQLiteConstraintException e) { + // Trying to log a revision that is already present + // Do nothing + success = true; + } finally { + if (cursor != null) { + cursor.close(); + } + } + return success; + } + + public void updateLogRevision(URL url, boolean push, TDRevision rev, + long lastUpdated) { + Object[] args = { "" + lastUpdated, url.toExternalForm(), + Integer.toString(push ? 1 : 0), rev.getDocId(), rev.getRevId() }; + + Cursor cursor = null; + try { + database.execSQL( + "UPDATE replicator_log SET lastUpdated = ? WHERE remote=? AND push=? AND docid=? AND revid=?", + args); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + public void removeLogForRevision(URL url, boolean push, TDRevision rev) { + Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), + rev.getDocId(), rev.getRevId() }; + Cursor cursor = null; + try { + database.execSQL( + "DELETE FROM replicator_log WHERE remote=? AND push=? AND docid=? AND revid=?", + args); + cursor = database.rawQuery("SELECT changes()", null); + if (cursor.moveToFirst() && cursor.getInt(0) > 0) { + // success = true; + } + } catch (SQLiteConstraintException e) { + // Trying to log a revision that is already present + // Do nothing + // success = true; + } finally { + if (cursor != null) { + cursor.close(); + } + } + // return success; + } + + public String lastSequenceWithRemoteURL(URL url, boolean push) { + Cursor cursor = null; + String result = null; + try { + String[] args = { url.toExternalForm(), + Integer.toString(push ? 1 : 0) }; + cursor = database + .rawQuery( + "SELECT last_sequence FROM replicators WHERE remote=? AND push=?", + args); + if (cursor.moveToFirst()) { + result = cursor.getString(0); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting last sequence", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + return result; + } + + public boolean setLastSequence(String lastSequence, URL url, boolean push) { + ContentValues values = new ContentValues(); + values.put("remote", url.toExternalForm()); + values.put("push", push); + values.put("last_sequence", lastSequence); + long newId = database.insertWithOnConflict("replicators", null, values, + SQLiteDatabase.CONFLICT_REPLACE); + return (newId == -1); + } + + public static String quote(String string) { + return string.replace("'", "''"); + } + + public static String joinQuoted(List strings) { + if (strings.size() == 0) { + return ""; + } + + String result = "'"; + boolean first = true; + for (String string : strings) { + if (first) { + first = false; + } else { + result = result + "','"; + } + result = result + quote(string); + } + result = result + "'"; + + return result; + } + + public boolean findMissingRevisions(TDRevisionList touchRevs) { + if (touchRevs.size() == 0) { + return true; + } + + String quotedDocIds = joinQuoted(touchRevs.getAllDocIds()); + String quotedRevIds = joinQuoted(touchRevs.getAllRevIds()); + + String sql = "SELECT docid, revid FROM revs, docs " + + "WHERE docid IN (" + quotedDocIds + ") AND revid in (" + + quotedRevIds + ")" + " AND revs.doc_id == docs.doc_id"; + + Cursor cursor = null; + try { + cursor = database.rawQuery(sql, null); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + TDRevision rev = touchRevs.revWithDocIdAndRevId( + cursor.getString(0), cursor.getString(1)); + + if (rev != null) { + touchRevs.remove(rev); + } + + cursor.moveToNext(); + } + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error finding missing revisions", e); + return false; + } finally { + if (cursor != null) { + cursor.close(); + } + } + return true; + } + + /*************************************************************************************************/ + /*** TDDatabase+LocalDocs ***/ + /*************************************************************************************************/ + + public TDRevision getLocalDocument(String docID, String revID) { + TDRevision result = null; + Cursor cursor = null; + try { + String[] args = { docID }; + cursor = database.rawQuery( + "SELECT revid, json FROM localdocs WHERE docid=?", args); + if (cursor.moveToFirst()) { + String gotRevID = cursor.getString(0); + if (revID != null && (!revID.equals(gotRevID))) { + return null; + } + byte[] json = cursor.getBlob(1); + Map properties = null; + try { + properties = TDServer.getObjectMapper().readValue(json, + Map.class); + properties.put("_id", docID); + properties.put("_rev", gotRevID); + result = new TDRevision(docID, gotRevID, false); + result.setProperties(properties); + } catch (Exception e) { + Log.w(TAG, "Error parsing local doc JSON", e); + return null; + } + + } + return result; + } catch (SQLException e) { + Log.e(TDDatabase.TAG, "Error getting local document", e); + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + public TDRevision putLocalRevision(TDRevision revision, String prevRevID, + TDStatus status) { + String docID = revision.getDocId(); + if (!docID.startsWith("_local/")) { + status.setCode(TDStatus.BAD_REQUEST); + return null; + } + + if (!revision.isDeleted()) { + // PUT: + byte[] json = encodeDocumentJSON(revision); + String newRevID; + if (prevRevID != null) { + int generation = TDRevision.generationFromRevID(prevRevID); + if (generation == 0) { + status.setCode(TDStatus.BAD_REQUEST); + return null; + } + newRevID = Integer.toString(++generation) + "-local"; + ContentValues values = new ContentValues(); + values.put("revid", newRevID); + values.put("json", json); + String[] whereArgs = { docID, prevRevID }; + try { + int rowsUpdated = database.update("localdocs", values, + "docid=? AND revid=?", whereArgs); + if (rowsUpdated == 0) { + status.setCode(TDStatus.CONFLICT); + return null; + } + } catch (SQLException e) { + status.setCode(TDStatus.INTERNAL_SERVER_ERROR); + return null; + } + } else { + newRevID = "1-local"; + ContentValues values = new ContentValues(); + values.put("docid", docID); + values.put("revid", newRevID); + values.put("json", json); + try { + database.insertWithOnConflict("localdocs", null, values, + SQLiteDatabase.CONFLICT_IGNORE); + } catch (SQLException e) { + status.setCode(TDStatus.INTERNAL_SERVER_ERROR); + return null; + } + } + status.setCode(TDStatus.CREATED); + return revision.copyWithDocID(docID, newRevID); + } else { + // DELETE: + TDStatus deleteStatus = deleteLocalDocument(docID, prevRevID); + status.setCode(deleteStatus.getCode()); + return (status.isSuccessful()) ? revision : null; + } + } + + public TDStatus deleteLocalDocument(String docID, String revID) { + if (docID == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + if (revID == null) { + // Didn't specify a revision to delete: 404 or a 409, depending + return (getLocalDocument(docID, null) != null) ? new TDStatus( + TDStatus.CONFLICT) : new TDStatus(TDStatus.NOT_FOUND); + } + String[] whereArgs = { docID, revID }; try { - database.execSQL("DELETE FROM replicator_log WHERE remote=? AND push=? AND docid=? AND revid=?", args); - cursor = database.rawQuery("SELECT changes()", null); - if(cursor.moveToFirst() && cursor.getInt(0)>0){ - //success = true; - } - }catch(SQLiteConstraintException e){ - // Trying to log a revision that is already present - // Do nothing - //success = true; - } finally { - if(cursor != null) { - cursor.close(); - } - } - //return success; - } - - public String lastSequenceWithRemoteURL(URL url, boolean push) { - Cursor cursor = null; - String result = null; - try { - String[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0) }; - cursor = database.rawQuery("SELECT last_sequence FROM replicators WHERE remote=? AND push=?", args); - if(cursor.moveToFirst()) { - result = cursor.getString(0); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting last sequence", e); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - return result; - } - - public boolean setLastSequence(String lastSequence, URL url, boolean push) { - ContentValues values = new ContentValues(); - values.put("remote", url.toExternalForm()); - values.put("push", push); - values.put("last_sequence", lastSequence); - long newId = database.insertWithOnConflict("replicators", null, values, SQLiteDatabase.CONFLICT_REPLACE); - return (newId == -1); - } - - public static String quote(String string) { - return string.replace("'", "''"); - } - - public static String joinQuoted(List strings) { - if(strings.size() == 0) { - return ""; - } - - String result = "'"; - boolean first = true; - for (String string : strings) { - if(first) { - first = false; - } - else { - result = result + "','"; - } - result = result + quote(string); - } - result = result + "'"; - - return result; - } - - public boolean findMissingRevisions(TDRevisionList touchRevs) { - if(touchRevs.size() == 0) { - return true; - } - - String quotedDocIds = joinQuoted(touchRevs.getAllDocIds()); - String quotedRevIds = joinQuoted(touchRevs.getAllRevIds()); - - String sql = "SELECT docid, revid FROM revs, docs " + - "WHERE docid IN (" + - quotedDocIds + - ") AND revid in (" + - quotedRevIds + ")" + - " AND revs.doc_id == docs.doc_id"; - - Cursor cursor = null; - try { - cursor = database.rawQuery(sql, null); - cursor.moveToFirst(); - while(!cursor.isAfterLast()) { - TDRevision rev = touchRevs.revWithDocIdAndRevId(cursor.getString(0), cursor.getString(1)); - - if(rev != null) { - touchRevs.remove(rev); - } - - cursor.moveToNext(); - } - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error finding missing revisions", e); - return false; - } finally { - if(cursor != null) { - cursor.close(); - } - } - return true; - } - - /*************************************************************************************************/ - /*** TDDatabase+LocalDocs ***/ - /*************************************************************************************************/ - - public TDRevision getLocalDocument(String docID, String revID) { - TDRevision result = null; - Cursor cursor = null; - try { - String[] args = { docID }; - cursor = database.rawQuery("SELECT revid, json FROM localdocs WHERE docid=?", args); - if(cursor.moveToFirst()) { - String gotRevID = cursor.getString(0); - if(revID != null && (!revID.equals(gotRevID))) { - return null; - } - byte[] json = cursor.getBlob(1); - Map properties = null; - try { - properties = TDServer.getObjectMapper().readValue(json, Map.class); - properties.put("_id", docID); - properties.put("_rev", gotRevID); - result = new TDRevision(docID, gotRevID, false); - result.setProperties(properties); - } catch (Exception e) { - Log.w(TAG, "Error parsing local doc JSON", e); - return null; - } - - } - return result; - } catch (SQLException e) { - Log.e(TDDatabase.TAG, "Error getting local document", e); - return null; - } finally { - if(cursor != null) { - cursor.close(); - } - } - } - - public TDRevision putLocalRevision(TDRevision revision, String prevRevID, TDStatus status) { - String docID = revision.getDocId(); - if(!docID.startsWith("_local/")) { - status.setCode(TDStatus.BAD_REQUEST); - return null; - } - - if(!revision.isDeleted()) { - // PUT: - byte[] json = encodeDocumentJSON(revision); - String newRevID; - if(prevRevID != null) { - int generation = TDRevision.generationFromRevID(prevRevID); - if(generation == 0) { - status.setCode(TDStatus.BAD_REQUEST); - return null; - } - newRevID = Integer.toString(++generation) + "-local"; - ContentValues values = new ContentValues(); - values.put("revid", newRevID); - values.put("json", json); - String[] whereArgs = { docID, prevRevID }; - try { - int rowsUpdated = database.update("localdocs", values, "docid=? AND revid=?", whereArgs); - if(rowsUpdated == 0) { - status.setCode(TDStatus.CONFLICT); - return null; - } - } catch (SQLException e) { - status.setCode(TDStatus.INTERNAL_SERVER_ERROR); - return null; - } - } else { - newRevID = "1-local"; - ContentValues values = new ContentValues(); - values.put("docid", docID); - values.put("revid", newRevID); - values.put("json", json); - try { - database.insertWithOnConflict("localdocs", null, values, SQLiteDatabase.CONFLICT_IGNORE); - } catch (SQLException e) { - status.setCode(TDStatus.INTERNAL_SERVER_ERROR); - return null; - } - } - status.setCode(TDStatus.CREATED); - return revision.copyWithDocID(docID, newRevID); - } - else { - // DELETE: - TDStatus deleteStatus = deleteLocalDocument(docID, prevRevID); - status.setCode(deleteStatus.getCode()); - return (status.isSuccessful()) ? revision : null; - } - } - - public TDStatus deleteLocalDocument(String docID, String revID) { - if(docID == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - if(revID == null) { - // Didn't specify a revision to delete: 404 or a 409, depending - return (getLocalDocument(docID, null) != null) ? new TDStatus(TDStatus.CONFLICT) : new TDStatus(TDStatus.NOT_FOUND); - } - String[] whereArgs = { docID, revID }; - try { - int rowsDeleted = database.delete("localdocs", "docid=? AND revid=?", whereArgs); - if(rowsDeleted == 0) { - return (getLocalDocument(docID, null) != null) ? new TDStatus(TDStatus.CONFLICT) : new TDStatus(TDStatus.NOT_FOUND); - } - return new TDStatus(TDStatus.OK); - } catch (SQLException e) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - } + int rowsDeleted = database.delete("localdocs", + "docid=? AND revid=?", whereArgs); + if (rowsDeleted == 0) { + return (getLocalDocument(docID, null) != null) ? new TDStatus( + TDStatus.CONFLICT) : new TDStatus(TDStatus.NOT_FOUND); + } + return new TDStatus(TDStatus.OK); + } catch (SQLException e) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + } } class TDValidationContextImpl implements TDValidationContext { - private TDDatabase database; - private TDRevision currentRevision; - private TDStatus errorType; - private String errorMessage; - - public TDValidationContextImpl(TDDatabase database, TDRevision currentRevision) { - this.database = database; - this.currentRevision = currentRevision; - this.errorType = new TDStatus(TDStatus.FORBIDDEN); - this.errorMessage = "invalid document"; - } - - @Override - public TDRevision getCurrentRevision() { - if(currentRevision != null) { - database.loadRevisionBody(currentRevision, EnumSet.noneOf(TDContentOptions.class)); - } - return currentRevision; - } - - @Override - public TDStatus getErrorType() { - return errorType; - } - - @Override - public void setErrorType(TDStatus status) { - this.errorType = status; - } - - @Override - public String getErrorMessage() { - return errorMessage; - } - - @Override - public void setErrorMessage(String message) { - this.errorMessage = message; - } + private TDDatabase database; + private TDRevision currentRevision; + private TDStatus errorType; + private String errorMessage; + + public TDValidationContextImpl(TDDatabase database, + TDRevision currentRevision) { + this.database = database; + this.currentRevision = currentRevision; + this.errorType = new TDStatus(TDStatus.FORBIDDEN); + this.errorMessage = "invalid document"; + } + + @Override + public TDRevision getCurrentRevision() { + if (currentRevision != null) { + database.loadRevisionBody(currentRevision, + EnumSet.noneOf(TDContentOptions.class)); + } + return currentRevision; + } + + @Override + public TDStatus getErrorType() { + return errorType; + } + + @Override + public void setErrorType(TDStatus status) { + this.errorType = status; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + + @Override + public void setErrorMessage(String message) { + this.errorMessage = message; + } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index cb57ef2..a190a2c 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -15,7 +16,6 @@ import android.database.SQLException; import android.util.Log; -import com.couchbase.touchdb.TDBody; import com.couchbase.touchdb.TDDatabase; import com.couchbase.touchdb.TDMisc; import com.couchbase.touchdb.TDRevision; @@ -29,7 +29,6 @@ import com.couchbase.touchdb.support.TDBatchProcessor; import com.couchbase.touchdb.support.TDBatcher; import com.couchbase.touchdb.support.TDRemoteRequestCompletionBlock; -import com.couchbase.touchdb.support.TDSequenceMap; public class TDPuller extends TDReplicator implements TDChangeTrackerClient { @@ -40,7 +39,6 @@ public class TDPuller extends TDReplicator implements TDChangeTrackerClient { protected long nextFakeSequence; protected long maxInsertedFakeSequence; protected TDChangeTracker changeTracker; - protected TDSequenceMap pendingSequences; protected int httpConnectionCount; @@ -57,6 +55,8 @@ public TDPuller(TDDatabase db, URL remote, String access_token, @Override public void beginReplicating() { + super.beginReplicating(); + if (downloadsToInsert == null) { downloadsToInsert = new TDBatcher>(workExecutor, 200, 1000, new TDBatchProcessor>() { @@ -119,42 +119,55 @@ public void stopped() { // Got a _changes feed entry from the TDChangeTracker. @Override public void changeTrackerReceivedChange(Map change) { - String lastSequence = change.get("seq").toString(); - String docID = (String) change.get("id"); - if (docID == null) { - return; - } - if (!TDDatabase.isValidDocumentId(docID)) { - Log.w(TDDatabase.TAG, String.format( - "%s: Received invalid doc ID from _changes: %s", this, - change)); - return; - } - boolean deleted = (change.containsKey("deleted") && ((Boolean) change - .get("deleted")).equals(Boolean.TRUE)); - List> changes = (List>) change - .get("changes"); - ArrayList revs = new ArrayList(); - for (Map changeDict : changes) { - String revID = (String) changeDict.get("rev"); - if (revID == null) { - continue; + // When there are no changes, we just send an empty map back + if (change.containsKey("id")) { + String lastSequence = change.get("seq").toString(); + String docID = (String) change.get("id"); + if (docID == null) { + return; + } + if (!TDDatabase.isValidDocumentId(docID)) { + Log.w(TDDatabase.TAG, String.format( + "%s: Received invalid doc ID from _changes: %s", this, + change)); + return; + } + boolean deleted = (change.containsKey("deleted") && ((Boolean) change + .get("deleted")).equals(Boolean.TRUE)); + List> changes = (List>) change + .get("changes"); + ArrayList revs = new ArrayList(); + for (Map changeDict : changes) { + String revID = (String) changeDict.get("rev"); + if (revID == null) { + continue; + } + TDRevision rev = new TDRevision(docID, revID, deleted); + // rev.setRemoteSequenceID(lastSequence); + rev.setSequence(++nextFakeSequence); + // addToInbox(rev); + revs.add(rev); + } + if (logRevisions(revs)) { + setChangesTotal(getChangesTotal() + changes.size()); + + // We set the sequence to ensure that changes tracker keeps + // moving forward. The docs pull eventually catches up. Filters + // are quite slow on CouchDB if you are pulling changes + // from the beginning, we want to retain as much progress we + // have + // made as possible + setLastSequence(lastSequence); } - TDPulledRevision rev = new TDPulledRevision(docID, revID, deleted); - rev.setRemoteSequenceID(lastSequence); - rev.setSequence(++nextFakeSequence); - // addToInbox(rev); - revs.add(rev); } - if (logRevisions(revs)) { - setChangesTotal(getChangesTotal() + changes.size()); - - // We set the sequence to ensure that changes tracker keeps - // moving forward. The docs pull eventually catches up. Filters - // are quite slow on CouchDB if you are pulling changes - // from the beginning, we want to retain as much progress we have - // made as possible - setLastSequence(lastSequence); + + // This is useful for the first run after the replicator starts + synchronized (pending_changes_running) { + if (!pending_changes_running.get()) { + pending_changes_running.set(true); + Log.d("ARTOOREFILLER", "Called by ChangeTracker"); + scheduleRefiller(); + } } } @@ -166,9 +179,9 @@ public void changeTrackerStopped(TDChangeTracker tracker) { // error = tracker.getError(); // } changeTracker = null; - if (batcher != null) { - batcher.flush(); - } + // if (batcher != null) { + // batcher.flush(); + // } asyncTaskFinished(1); } @@ -188,8 +201,8 @@ public void processInbox(TDRevisionList inbox) { // Ask the local database which of the revs are not known to it: // Log.w(TDDatabase.TAG, String.format("%s: Looking up %s", this, // inbox)); - String lastInboxSequence = ((TDPulledRevision) inbox - .get(inbox.size() - 1)).getRemoteSequenceID(); + // String lastInboxSequence = ((TDPulledRevision) inbox + // .get(inbox.size() - 1)).getRemoteSequenceID(); int total = getChangesTotal() - inbox.size(); if (!db.findMissingRevisions(inbox)) { Log.w(TDDatabase.TAG, @@ -210,9 +223,9 @@ public void processInbox(TDRevisionList inbox) { // Nothing to do. Just bump the lastSequence. Log.w(TDDatabase.TAG, String.format("%s no new remote revisions to fetch", this)); - long seq = pendingSequences.addValue(lastInboxSequence); - pendingSequences.removeSequence(seq); - setLastSequence(pendingSequences.getCheckpointedValue()); + // long seq = pendingSequences.addValue(lastInboxSequence); + // pendingSequences.removeSequence(seq); + // setLastSequence(pendingSequences.getCheckpointedValue()); return; } @@ -228,10 +241,10 @@ public void processInbox(TDRevisionList inbox) { } for (int i = 0; i < inbox.size(); i++) { - TDPulledRevision rev = (TDPulledRevision) inbox.get(i); + TDRevision rev = (TDRevision) inbox.get(i); // FIXME add logic here to pull initial revs in bulk - rev.setSequence(pendingSequences.addValue(rev - .getRemoteSequenceID())); + // rev.setSequence(pendingSequences.addValue(rev + // .getRemoteSequenceID())); revsToPull.add(rev); } } @@ -248,6 +261,18 @@ public void processInbox(TDRevisionList inbox) { * access */ public void pullRemoteRevisions() { + + // If we don't have any remote revisions, refill again + if (revsToPull.size() == 0) { + Log.d("ARTOOREFILLER", "Called by pullRemoteRevisions"); + scheduleRefiller(new Date().getTime()); + } else { + // resets the counter + // synchronized (refiller_scheduled) { + refiller_scheduled.set(false); + // } + } + // find the work to be done in a synchronized block List workToStartNow = new ArrayList(); synchronized (this) { @@ -269,6 +294,8 @@ public void pullRemoteRevisions() { * parent revision ID. The contents are stored into rev.properties. */ public void pullRemoteRevision(final TDRevision rev) { + updateLogRevision(rev, new Date().getTime()); + asyncTaskStarted(); ++httpConnectionCount; @@ -294,6 +321,8 @@ public void pullRemoteRevision(final TDRevision rev) { path.append(joinQuotedEscaped(knownRevs)); } + path.append("&access_token=").append(access_token); + // create a final version of this variable for the log statement inside // FIXME find a way to avoid this final String pathInside = path.toString(); @@ -340,7 +369,6 @@ public void onCompletion(Object result, Throwable e) { pullRemoteRevisions(); } }); - } /** @@ -386,7 +414,7 @@ public int compare(List list1, List list2) { boolean success = false; try { for (List revAndHistory : revs) { - TDPulledRevision rev = (TDPulledRevision) revAndHistory.get(0); + TDRevision rev = (TDRevision) revAndHistory.get(0); long fakeSequence = rev.getSequence(); List history = (List) revAndHistory.get(1); // Insert the revision: @@ -402,16 +430,14 @@ public int compare(List list1, List list2) { null); continue; } + } else { + removeLogForRevision(rev); } - - pendingSequences.removeSequence(fakeSequence); } Log.w(TDDatabase.TAG, this + " finished inserting " + revs.size() + " revisions"); - setLastSequence(pendingSequences.getCheckpointedValue()); - success = true; } catch (SQLException e) { Log.w(TDDatabase.TAG, this + ": Exception inserting revisions", e); @@ -445,33 +471,3 @@ public String joinQuotedEscaped(List strings) { } } - -/** - * A revision received from a remote server during a pull. Tracks the opaque - * remote sequence ID. - */ -class TDPulledRevision extends TDRevision { - - public TDPulledRevision(TDBody body) { - super(body); - } - - public TDPulledRevision(String docId, String revId, boolean deleted) { - super(docId, revId, deleted); - } - - public TDPulledRevision(Map properties) { - super(properties); - } - - protected String remoteSequenceID; - - public String getRemoteSequenceID() { - return remoteSequenceID; - } - - public void setRemoteSequenceID(String remoteSequenceID) { - this.remoteSequenceID = remoteSequenceID; - } - -} diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java index 857a04a..c199ab8 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java @@ -81,6 +81,8 @@ public void onCompletion(Object result, Throwable e) { @Override public void beginReplicating() { + super.beginReplicating(); + // If we're still waiting to create the remote db, do nothing now. (This // method will be // re-invoked after that request finishes; see maybeCreateRemoteDB() diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index c1930b9..2d3133b 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -11,6 +11,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.http.client.HttpClient; import org.apache.http.client.HttpResponseException; @@ -57,6 +58,8 @@ public abstract class TDReplicator extends Observable { protected static final int INBOX_CAPACITY = 100; protected String access_token = null; + protected AtomicBoolean pending_changes_running = new AtomicBoolean(false); + protected AtomicBoolean refiller_scheduled = new AtomicBoolean(false); private ExecutorService remoteRequestExecutor; @@ -82,8 +85,6 @@ public TDReplicator(TDDatabase db, URL remote, String access_token, @Override public void process(List inbox) { - inbox = TDReplicator.this.db.getPendingRevisions( - getRemote(), isPush()); Log.v(TDDatabase.TAG, "*** " + toString() + ": BEGIN processInbox (" + inbox.size() + " sequences)"); @@ -121,9 +122,6 @@ public URL getRemote() { } public void databaseClosing() { - // if (pendingChanges != null) { - // this.handler.removeCallbacks(pendingChanges); - // } saveLastSequence(); stop(); db = null; @@ -201,7 +199,9 @@ public void start() { fetchRemoteCheckpointDoc(); } - public abstract void beginReplicating(); + public void beginReplicating() { + // scheduleRefiller(); + } public void stop() { if (!running) { @@ -281,8 +281,12 @@ public boolean logRevision(TDRevision rev) { return this.db.logRevision(this.remote, isPush(), rev); } - public List getPendingRevisions() { - return this.db.getPendingRevisions(this.remote, isPush()); + public TDRevisionList getPendingRevisions(long lastUpdated) { + return this.db.getPendingRevisions(this.remote, isPush(), lastUpdated); + } + + public void updateLogRevision(TDRevision rev, long lastUpdated) { + this.db.updateLogRevision(getRemote(), isPush(), rev, lastUpdated); } public void removeLogForRevision(TDRevision rev) { @@ -416,4 +420,56 @@ public void onCompletion(Object result, Throwable e) { db.setLastSequence(lastSequence, remote, isPush()); } + protected class Refill implements Runnable { + + private long lastUpdated; + + public Refill() { + this(-1); + } + + public Refill(long lastUpdated) { + this.lastUpdated = lastUpdated; + } + + @Override + public void run() { + TDRevisionList revisions = getPendingRevisions(lastUpdated); + if (revisions.size() > 0) { + synchronized (pending_changes_running) { + pending_changes_running.set(true); + } + for (TDRevision rev : revisions) { + batcher.queueObject(rev); + } + } else { + // The first time we start replication, we will have zero + // changes. We will need to kick start replication when + // changeTracker receives changes + synchronized (pending_changes_running) { + pending_changes_running.set(false); + } + } + // synchronized (refiller_scheduled) { + // refiller_scheduled.set(false); + // } + } + } + + protected void scheduleRefiller() { + scheduleRefiller(-1); + } + + protected void scheduleRefiller(long lastUpdated) { + synchronized (refiller_scheduled) { + if (!refiller_scheduled.get()) { + refiller_scheduled.set(true); + workExecutor.submit(new Refill(lastUpdated)); + Log.d("ARTOOREFILLER","started with --" + lastUpdated); + } else { + Log.d("ARTOOREFILLER","Didn't start"); + } + } + } + } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java index 6d093fc..2b0cd26 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java @@ -7,6 +7,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -213,18 +214,33 @@ public void process(HttpRequest request, try { InputStream input = entity.getContent(); if (mode == TDChangeTrackerMode.LongPoll) { - Map fullBody = TDServer - .getObjectMapper().readValue(input, - Map.class); - boolean responseOK = receivedPollResponse(fullBody); - if (mode == TDChangeTrackerMode.LongPoll - && responseOK) { - Log.v(TDDatabase.TAG, "Starting new longpoll"); + BufferedReader reader = new BufferedReader( + new InputStreamReader(input)); + String line = null; + StringBuffer sb = new StringBuffer(); + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + String content = sb.toString(); + if ("{\"results\":[".equals(content)) { + // No more pending changes; send an empty map + client.changeTrackerReceivedChange(new HashMap()); continue; } else { - Log.w(TDDatabase.TAG, - "Change tracker calling stop"); - stop(); + Map fullBody = TDServer + .getObjectMapper().readValue(content, + Map.class); + boolean responseOK = receivedPollResponse(fullBody); + if (mode == TDChangeTrackerMode.LongPoll + && responseOK) { + Log.v(TDDatabase.TAG, + "Starting new longpoll"); + continue; + } else { + Log.w(TDDatabase.TAG, + "Change tracker calling stop"); + stop(); + } } } else { BufferedReader reader = new BufferedReader( From 10d0a0d90f2e030b4a8a920592c3bab207c81f27 Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Sat, 25 May 2013 10:31:56 +0530 Subject: [PATCH 08/11] Tweaked replicators --- TouchDB-Android/.classpath | 1 + .../src/com/couchbase/touchdb/TDDatabase.java | 45 ++++++++++++++++++- .../touchdb/replicator/TDPuller.java | 13 ++---- .../touchdb/replicator/TDPusher.java | 15 ++++++- .../touchdb/replicator/TDReplicator.java | 42 +++++++++++------ .../changetracker/TDChangeTracker.java | 5 +-- 6 files changed, 91 insertions(+), 30 deletions(-) diff --git a/TouchDB-Android/.classpath b/TouchDB-Android/.classpath index 0e46cdf..bb8a0a9 100644 --- a/TouchDB-Android/.classpath +++ b/TouchDB-Android/.classpath @@ -7,5 +7,6 @@ + diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index f88cb1b..264a3e8 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -2441,8 +2441,8 @@ public TDRevisionList getPendingRevisions(URL url, boolean push, url.toExternalForm(), Integer.toString(push ? 1 : 0), "" - + ((lastUpdated == -1 ? new Date().getTime() - : lastUpdated) - (60 * 1000)) }; + + (lastUpdated == -1 ? new Date().getTime() + : lastUpdated - (60 * 1000 * 5)) }; Cursor cursor = database .rawQuery( "SELECT docid, revid, deleted, sequence, lastUpdated FROM replicator_log WHERE (remote=? AND push=?) AND (lastUpdated IS NULL OR lastUpdated < ?) LIMIT 100", @@ -2468,6 +2468,32 @@ public TDRevisionList getPendingRevisions(URL url, boolean push, return result; } + public Map getPendingRevisionStats() { + + Cursor cursor = database + .rawQuery( + "SELECT push, count(*) FROM replicator_log group by push", + null); + HashMap map = new HashMap(); + map.put("push", 0); + map.put("pull", 0); + + if (cursor.moveToFirst()) { + do { + if (cursor.getInt(0) == 0) { + map.put("pull", cursor.getInt(1)); + } else { + map.put("push", cursor.getInt(1)); + } + } while (cursor.moveToNext()); + } + + if (cursor != null) { + cursor.close(); + } + return map; + } + public boolean logRevision(URL url, boolean push, TDRevision rev) { boolean success = false; Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0), @@ -2535,6 +2561,21 @@ public void removeLogForRevision(URL url, boolean push, TDRevision rev) { // return success; } + public void resetRevisions(URL url, boolean push) { + Object[] args = { url.toExternalForm(), Integer.toString(push ? 1 : 0) }; + + Cursor cursor = null; + try { + database.execSQL( + "UPDATE replicator_log SET lastUpdated = 0 WHERE remote=? AND push=?", + args); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + public String lastSequenceWithRemoteURL(URL url, boolean push) { Cursor cursor = null; String result = null; diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index a190a2c..97633fc 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -55,7 +55,6 @@ public TDPuller(TDDatabase db, URL remote, String access_token, @Override public void beginReplicating() { - super.beginReplicating(); if (downloadsToInsert == null) { downloadsToInsert = new TDBatcher>(workExecutor, 200, @@ -161,14 +160,7 @@ public void changeTrackerReceivedChange(Map change) { } } - // This is useful for the first run after the replicator starts - synchronized (pending_changes_running) { - if (!pending_changes_running.get()) { - pending_changes_running.set(true); - Log.d("ARTOOREFILLER", "Called by ChangeTracker"); - scheduleRefiller(); - } - } + super.beginReplicating(); } @Override @@ -183,6 +175,9 @@ public void changeTrackerStopped(TDChangeTracker tracker) { // batcher.flush(); // } + // If the tracker is not working we need to stop this replicator + stop(); + asyncTaskFinished(1); } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java index c199ab8..8fc70b9 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java @@ -2,6 +2,7 @@ import java.net.URL; import java.util.ArrayList; +import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.List; @@ -81,7 +82,6 @@ public void onCompletion(Object result, Throwable e) { @Override public void beginReplicating() { - super.beginReplicating(); // If we're still waiting to create the remote db, do nothing now. (This // method will be @@ -125,6 +125,8 @@ public void beginReplicating() { asyncTaskStarted(); // prevents stopped() from being called when // other tasks finish } + + super.beginReplicating(); } @Override @@ -163,12 +165,16 @@ public void update(Observable observable, Object data) { } } + super.beginReplicating(); } @Override - public void processInbox(final TDRevisionList inbox) { + public void processInbox(final TDRevisionList inbox) { if (inbox.size() == 0) { + scheduleRefiller(); return; + } else { + refiller_scheduled.set(false); } final long lastInboxSequence = inbox.get(inbox.size() - 1) @@ -184,6 +190,7 @@ public void processInbox(final TDRevisionList inbox) { diffs.put(docID, revs); } revs.add(rev.getRevId()); + updateLogRevision(rev, new Date().getTime()); } // Call _revs_diff on the target db: @@ -295,6 +302,8 @@ public void onCompletion(Object result, setChangesProcessed(getChangesProcessed() + numDocsToSend); asyncTaskFinished(1); + + scheduleRefiller(new Date().getTime()); } }); @@ -309,6 +318,8 @@ public void onCompletion(Object result, removeLogForRevision(rev); } db.endTransaction(true); + + scheduleRefiller(new Date().getTime()); } asyncTaskFinished(1); } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index 2d3133b..e678644 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -58,7 +58,8 @@ public abstract class TDReplicator extends Observable { protected static final int INBOX_CAPACITY = 100; protected String access_token = null; - protected AtomicBoolean pending_changes_running = new AtomicBoolean(false); + // protected AtomicBoolean pending_changes_running = new + // AtomicBoolean(false); protected AtomicBoolean refiller_scheduled = new AtomicBoolean(false); private ExecutorService remoteRequestExecutor; @@ -200,7 +201,14 @@ public void start() { } public void beginReplicating() { - // scheduleRefiller(); + // This is useful for the first run after the replicator starts + synchronized (refiller_scheduled) { + if (!refiller_scheduled.get()) { + refiller_scheduled.set(true); + Log.d("ARTOOREFILLER", "Called by ChangeTracker"); + scheduleRefiller(); + } + } } public void stop() { @@ -213,6 +221,10 @@ public void stop() { if (asyncTaskCount == 0) { stopped(); } + + // All the revisions that have a timestamp for this replicator are reset + // to 0. So that they get picked up in the next run. + resetRevisions(); } public void stopped() { @@ -222,6 +234,10 @@ public void stopped() { saveLastSequence(); + if (db != null) { + db.getActiveReplicators().remove(this); + } + batcher = null; db = null; } @@ -293,6 +309,10 @@ public void removeLogForRevision(TDRevision rev) { this.db.removeLogForRevision(this.remote, isPush(), rev); } + public void resetRevisions() { + this.db.resetRevisions(this.remote, isPush()); + } + /** CHECKPOINT STORAGE: **/ public void maybeCreateRemoteDB() { @@ -429,16 +449,13 @@ public Refill() { } public Refill(long lastUpdated) { - this.lastUpdated = lastUpdated; + this.lastUpdated = lastUpdated; } @Override - public void run() { + public void run() { TDRevisionList revisions = getPendingRevisions(lastUpdated); if (revisions.size() > 0) { - synchronized (pending_changes_running) { - pending_changes_running.set(true); - } for (TDRevision rev : revisions) { batcher.queueObject(rev); } @@ -446,13 +463,10 @@ public void run() { // The first time we start replication, we will have zero // changes. We will need to kick start replication when // changeTracker receives changes - synchronized (pending_changes_running) { - pending_changes_running.set(false); + synchronized (refiller_scheduled) { + refiller_scheduled.set(false); } } - // synchronized (refiller_scheduled) { - // refiller_scheduled.set(false); - // } } } @@ -465,9 +479,9 @@ protected void scheduleRefiller(long lastUpdated) { if (!refiller_scheduled.get()) { refiller_scheduled.set(true); workExecutor.submit(new Refill(lastUpdated)); - Log.d("ARTOOREFILLER","started with --" + lastUpdated); + Log.d("ARTOOREFILLER", "started with --" + lastUpdated); } else { - Log.d("ARTOOREFILLER","Didn't start"); + Log.d("ARTOOREFILLER", "Didn't start"); } } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java index 2b0cd26..d3e5c54 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java @@ -222,7 +222,7 @@ public void process(HttpRequest request, sb.append(line).append("\n"); } String content = sb.toString(); - if ("{\"results\":[".equals(content)) { + if ("{\"results\":[".equals(content.trim())) { // No more pending changes; send an empty map client.changeTrackerReceivedChange(new HashMap()); continue; @@ -231,8 +231,7 @@ public void process(HttpRequest request, .getObjectMapper().readValue(content, Map.class); boolean responseOK = receivedPollResponse(fullBody); - if (mode == TDChangeTrackerMode.LongPoll - && responseOK) { + if (responseOK) { Log.v(TDDatabase.TAG, "Starting new longpoll"); continue; From a79022786a165c84a1b63a8ca02f0eb1dca5acee Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Sun, 26 May 2013 01:39:48 +0530 Subject: [PATCH 09/11] Tweaked replicator push/pull -- fixed comparison of source of data with Pull remote url --- .../src/com/couchbase/touchdb/TDDatabase.java | 6 +++--- .../com/couchbase/touchdb/replicator/TDPuller.java | 2 +- .../com/couchbase/touchdb/replicator/TDPusher.java | 2 +- .../couchbase/touchdb/replicator/TDReplicator.java | 13 +++++-------- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index 264a3e8..56f6886 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -1976,7 +1976,7 @@ public byte[] encodeDocumentJSON(TDRevision rev) { return json; } - public void notifyChange(TDRevision rev, URL source) { + public void notifyChange(TDRevision rev, String source) { Map changeNotification = new HashMap(); changeNotification.put("rev", rev); changeNotification.put("seq", rev.getSequence()); @@ -2237,7 +2237,7 @@ && existsDocumentWithIDAndRev(docId, null)) { * already exist locally will create phantom revisions with no content. */ public TDStatus forceInsert(TDRevision rev, List revHistory, - URL source) { + String source) { String docId = rev.getDocId(); String revId = rev.getRevId(); @@ -2445,7 +2445,7 @@ public TDRevisionList getPendingRevisions(URL url, boolean push, : lastUpdated - (60 * 1000 * 5)) }; Cursor cursor = database .rawQuery( - "SELECT docid, revid, deleted, sequence, lastUpdated FROM replicator_log WHERE (remote=? AND push=?) AND (lastUpdated IS NULL OR lastUpdated < ?) LIMIT 100", + "SELECT docid, revid, deleted, sequence, lastUpdated FROM replicator_log WHERE remote=? AND push=? AND lastUpdated < ? LIMIT 100", args); if (cursor.moveToFirst()) { do { diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index 97633fc..0a89173 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -413,7 +413,7 @@ public int compare(List list1, List list2) { long fakeSequence = rev.getSequence(); List history = (List) revAndHistory.get(1); // Insert the revision: - TDStatus status = db.forceInsert(rev, history, remote); + TDStatus status = db.forceInsert(rev, history, remote.toExternalForm()); if (!status.isSuccessful()) { if (status.getCode() == TDStatus.FORBIDDEN) { Log.i(TDDatabase.TAG, this diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java index 8fc70b9..d78a866 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java @@ -150,7 +150,7 @@ public void update(Observable observable, Object data) { Map change = (Map) data; // Skip revisions that originally came from the database I'm syncing // to: - URL source = (URL) change.get("source"); + String source = (String) change.get("source"); if (source != null && source.equals(remote.toExternalForm())) { return; } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index e678644..46acd77 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -202,13 +202,8 @@ public void start() { public void beginReplicating() { // This is useful for the first run after the replicator starts - synchronized (refiller_scheduled) { - if (!refiller_scheduled.get()) { - refiller_scheduled.set(true); - Log.d("ARTOOREFILLER", "Called by ChangeTracker"); - scheduleRefiller(); - } - } + Log.d("ARTOOREFILLER", "Called by ChangeTracker"); + scheduleRefiller(); } public void stop() { @@ -310,7 +305,9 @@ public void removeLogForRevision(TDRevision rev) { } public void resetRevisions() { - this.db.resetRevisions(this.remote, isPush()); + if (this.db != null) { + this.db.resetRevisions(this.remote, isPush()); + } } /** CHECKPOINT STORAGE: **/ From bb946c8aa36bb0f83d52ccd255e35513ce4f7efe Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Mon, 8 Jul 2013 18:27:15 +0530 Subject: [PATCH 10/11] Push and Pull are working well --- TouchDB-Android-Ektorp/.classpath | 1 + TouchDB-Android-Ektorp/lint.xml | 3 + TouchDB-Android-Listener/.classpath | 1 + TouchDB-Android/.classpath | 1 + .../org.hibernate.eclipse.console.prefs | 4 + .../src/com/couchbase/touchdb/TDDatabase.java | 14 +- .../touchdb/replicator/TDPuller.java | 28 +- .../touchdb/replicator/TDReplicator.java | 14 +- .../changetracker/TDChangeTracker.java | 3 + .../couchbase/touchdb/router/TDRouter.java | 2913 +++++++++-------- .../touchdb/support/TDRemoteRequest.java | 1 - 11 files changed, 1575 insertions(+), 1408 deletions(-) create mode 100644 TouchDB-Android-Ektorp/lint.xml create mode 100644 TouchDB-Android/.settings/org.hibernate.eclipse.console.prefs diff --git a/TouchDB-Android-Ektorp/.classpath b/TouchDB-Android-Ektorp/.classpath index 1c706e2..ce767aa 100644 --- a/TouchDB-Android-Ektorp/.classpath +++ b/TouchDB-Android-Ektorp/.classpath @@ -6,5 +6,6 @@ + diff --git a/TouchDB-Android-Ektorp/lint.xml b/TouchDB-Android-Ektorp/lint.xml new file mode 100644 index 0000000..ee0eead --- /dev/null +++ b/TouchDB-Android-Ektorp/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/TouchDB-Android-Listener/.classpath b/TouchDB-Android-Listener/.classpath index cecf57d..dd64e62 100644 --- a/TouchDB-Android-Listener/.classpath +++ b/TouchDB-Android-Listener/.classpath @@ -6,5 +6,6 @@ + diff --git a/TouchDB-Android/.classpath b/TouchDB-Android/.classpath index bb8a0a9..785d3a4 100644 --- a/TouchDB-Android/.classpath +++ b/TouchDB-Android/.classpath @@ -8,5 +8,6 @@ + diff --git a/TouchDB-Android/.settings/org.hibernate.eclipse.console.prefs b/TouchDB-Android/.settings/org.hibernate.eclipse.console.prefs new file mode 100644 index 0000000..1f6dcec --- /dev/null +++ b/TouchDB-Android/.settings/org.hibernate.eclipse.console.prefs @@ -0,0 +1,4 @@ +#Sat Mar 30 13:04:34 GMT+05:30 2013 +default.configuration= +eclipse.preferences.version=1 +hibernate3.enabled=false diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index 56f6886..16bde41 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -2441,8 +2441,8 @@ public TDRevisionList getPendingRevisions(URL url, boolean push, url.toExternalForm(), Integer.toString(push ? 1 : 0), "" - + (lastUpdated == -1 ? new Date().getTime() - : lastUpdated - (60 * 1000 * 5)) }; + + ((lastUpdated == -1 ? new Date().getTime() + : lastUpdated) - (60 * 1000 * 1)) }; Cursor cursor = database .rawQuery( "SELECT docid, revid, deleted, sequence, lastUpdated FROM replicator_log WHERE remote=? AND push=? AND lastUpdated < ? LIMIT 100", @@ -2634,9 +2634,10 @@ public static String joinQuoted(List strings) { return result; } - public boolean findMissingRevisions(TDRevisionList touchRevs) { + public TDRevisionList findMissingRevisions(TDRevisionList touchRevs) { + TDRevisionList removalList = new TDRevisionList(); if (touchRevs.size() == 0) { - return true; + return removalList; } String quotedDocIds = joinQuoted(touchRevs.getAllDocIds()); @@ -2656,19 +2657,20 @@ public boolean findMissingRevisions(TDRevisionList touchRevs) { if (rev != null) { touchRevs.remove(rev); + removalList.add(rev); } cursor.moveToNext(); } } catch (SQLException e) { Log.e(TDDatabase.TAG, "Error finding missing revisions", e); - return false; + return null; } finally { if (cursor != null) { cursor.close(); } } - return true; + return removalList; } /*************************************************************************************************/ diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index 0a89173..4a5c0ae 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -55,6 +55,7 @@ public TDPuller(TDDatabase db, URL remote, String access_token, @Override public void beginReplicating() { + super.beginReplicating(); if (downloadsToInsert == null) { downloadsToInsert = new TDBatcher>(workExecutor, 200, @@ -160,7 +161,16 @@ public void changeTrackerReceivedChange(Map change) { } } - super.beginReplicating(); + // If we don't have anything in the buffer + if (revsToPull == null) { + super.beginReplicating(); + } else { + synchronized (revsToPull) { + if (revsToPull.size() == 0) { + super.beginReplicating(); + } + } + } } @Override @@ -199,10 +209,16 @@ public void processInbox(TDRevisionList inbox) { // String lastInboxSequence = ((TDPulledRevision) inbox // .get(inbox.size() - 1)).getRemoteSequenceID(); int total = getChangesTotal() - inbox.size(); - if (!db.findMissingRevisions(inbox)) { + TDRevisionList removalList; + if ((removalList = db.findMissingRevisions(inbox)) == null) { Log.w(TDDatabase.TAG, String.format("%s failed to look up local revs", this)); inbox = null; + } else { + // Remove all the entries we do not need to fetch + for (TDRevision rev : removalList) { + removeLogForRevision(rev); + } } // introducing this to java version since inbox may now be null // everywhere @@ -221,6 +237,8 @@ public void processInbox(TDRevisionList inbox) { // long seq = pendingSequences.addValue(lastInboxSequence); // pendingSequences.removeSequence(seq); // setLastSequence(pendingSequences.getCheckpointedValue()); + + refiller_scheduled.set(false); return; } @@ -259,11 +277,12 @@ public void pullRemoteRevisions() { // If we don't have any remote revisions, refill again if (revsToPull.size() == 0) { - Log.d("ARTOOREFILLER", "Called by pullRemoteRevisions"); + Log.d(getLogTag(), "Called by pullRemoteRevisions"); scheduleRefiller(new Date().getTime()); } else { // resets the counter // synchronized (refiller_scheduled) { + Log.d(getLogTag(), "refiller_scheduled flag set to false"); refiller_scheduled.set(false); // } } @@ -413,7 +432,8 @@ public int compare(List list1, List list2) { long fakeSequence = rev.getSequence(); List history = (List) revAndHistory.get(1); // Insert the revision: - TDStatus status = db.forceInsert(rev, history, remote.toExternalForm()); + TDStatus status = db.forceInsert(rev, history, + remote.toExternalForm()); if (!status.isSuccessful()) { if (status.getCode() == TDStatus.FORBIDDEN) { Log.i(TDDatabase.TAG, this diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index 46acd77..2ab2225 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -202,7 +202,7 @@ public void start() { public void beginReplicating() { // This is useful for the first run after the replicator starts - Log.d("ARTOOREFILLER", "Called by ChangeTracker"); + Log.d(getLogTag(), "Called by ChangeTracker"); scheduleRefiller(); } @@ -451,17 +451,21 @@ public Refill(long lastUpdated) { @Override public void run() { + Log.d(getLogTag(), isPush() ? "PUSH" : "PULL"); TDRevisionList revisions = getPendingRevisions(lastUpdated); if (revisions.size() > 0) { for (TDRevision rev : revisions) { batcher.queueObject(rev); } + Log.d(getLogTag(), "Revs count: " + revisions.size() + + ", should I have set the flag?"); } else { // The first time we start replication, we will have zero // changes. We will need to kick start replication when // changeTracker receives changes synchronized (refiller_scheduled) { refiller_scheduled.set(false); + Log.d(getLogTag(), "I set scheduled to false"); } } } @@ -474,13 +478,17 @@ protected void scheduleRefiller() { protected void scheduleRefiller(long lastUpdated) { synchronized (refiller_scheduled) { if (!refiller_scheduled.get()) { + Log.d(getLogTag(), "started with --" + lastUpdated); refiller_scheduled.set(true); workExecutor.submit(new Refill(lastUpdated)); - Log.d("ARTOOREFILLER", "started with --" + lastUpdated); + Log.d(getLogTag(), "started with --" + lastUpdated); } else { - Log.d("ARTOOREFILLER", "Didn't start"); + Log.d(getLogTag(), "Didn't start"); } } } + protected String getLogTag() { + return "ARTOOREFILLER" + (isPush() ? "PUSH" : "PULL"); + } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java index d3e5c54..eb29c0b 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/changetracker/TDChangeTracker.java @@ -273,11 +273,14 @@ public void process(HttpRequest request, } catch (ClientProtocolException e) { Log.e(TDDatabase.TAG, "ClientProtocolException in change tracker", e); + stop(); } catch (IOException e) { if (running) { // we get an exception when we're shutting down and have to // close the socket underneath our read, ignore that Log.e(TDDatabase.TAG, "IOException in change tracker", e); + // The tracker keeps trying again and again + stop(); } } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java b/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java index a872e87..32f82ee 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java @@ -41,1400 +41,1525 @@ import com.couchbase.touchdb.replicator.TDPusher; import com.couchbase.touchdb.replicator.TDReplicator; - public class TDRouter implements Observer { - private TDServer server; - private TDDatabase db; - private TDURLConnection connection; - private Map queries; - private boolean changesIncludesDocs = false; - private TDRouterCallbackBlock callbackBlock; - private boolean responseSent = false; - private boolean waiting = false; - private TDFilterBlock changesFilter; - private boolean longpoll = false; - - public static String getVersionString() { - return TouchDBVersion.TouchDBVersionNumber; - } - - public TDRouter(TDServer server, TDURLConnection connection) { - this.server = server; - this.connection = connection; - } - - public void setCallbackBlock(TDRouterCallbackBlock callbackBlock) { - this.callbackBlock = callbackBlock; - } - - public Map getQueries() { - if(queries == null) { - String queryString = connection.getURL().getQuery(); - if(queryString != null && queryString.length() > 0) { - queries = new HashMap(); - for (String component : queryString.split("&")) { - int location = component.indexOf('='); - if(location > 0) { - String key = component.substring(0, location); - String value = component.substring(location + 1); - queries.put(key, value); - } - } - - } - } - return queries; - } - - public String getQuery(String param) { - Map queries = getQueries(); - if(queries != null) { - String value = queries.get(param); - if(value != null) { - return URLDecoder.decode(value); - } - } - return null; - } - - public boolean getBooleanQuery(String param) { - String value = getQuery(param); - return (value != null) && !"false".equals(value) && !"0".equals(value); - } - - public int getIntQuery(String param, int defaultValue) { - int result = defaultValue; - String value = getQuery(param); - if(value != null) { - try { - result = Integer.parseInt(value); - } catch (NumberFormatException e) { - //ignore, will return default value - } - } - - return result; - } - - public Object getJSONQuery(String param) { - String value = getQuery(param); - if(value == null) { - return null; - } - Object result = null; - try { - result = TDServer.getObjectMapper().readValue(value, Object.class); - } catch (Exception e) { - Log.w("Unable to parse JSON Query", e); - } - return result; - } - - public boolean cacheWithEtag(String etag) { - String eTag = String.format("\"%s\"", etag); - connection.getResHeader().add("Etag", eTag); - String requestIfNoneMatch = connection.getRequestProperty("If-None-Match"); - return eTag.equals(requestIfNoneMatch); - } - - public Map getBodyAsDictionary() { - try { - InputStream contentStream = connection.getRequestInputStream(); - Map bodyMap = TDServer.getObjectMapper().readValue(contentStream, Map.class); - return bodyMap; - } catch (IOException e) { - return null; - } - } - - public EnumSet getContentOptions() { - EnumSet result = EnumSet.noneOf(TDContentOptions.class); - if(getBooleanQuery("attachments")) { - result.add(TDContentOptions.TDIncludeAttachments); - } - if(getBooleanQuery("local_seq")) { - result.add(TDContentOptions.TDIncludeLocalSeq); - } - if(getBooleanQuery("conflicts")) { - result.add(TDContentOptions.TDIncludeConflicts); - } - if(getBooleanQuery("revs")) { - result.add(TDContentOptions.TDIncludeRevs); - } - if(getBooleanQuery("revs_info")) { - result.add(TDContentOptions.TDIncludeRevsInfo); - } - return result; - } - - public boolean getQueryOptions(TDQueryOptions options) { - // http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options - options.setSkip(getIntQuery("skip", options.getSkip())); - options.setLimit(getIntQuery("limit", options.getLimit())); - options.setGroupLevel(getIntQuery("group_level", options.getGroupLevel())); - options.setDescending(getBooleanQuery("descending")); - options.setIncludeDocs(getBooleanQuery("include_docs")); - options.setUpdateSeq(getBooleanQuery("update_seq")); - if(getQuery("inclusive_end") != null) { - options.setInclusiveEnd(getBooleanQuery("inclusive_end")); - } - if(getQuery("reduce") != null) { - options.setReduce(getBooleanQuery("reduce")); - } - options.setGroup(getBooleanQuery("group")); - options.setContentOptions(getContentOptions()); - options.setStartKey(getJSONQuery("startkey")); - options.setEndKey(getJSONQuery("endkey")); - Object key = getJSONQuery("key"); - if(key != null) { - List keys = new ArrayList(); - keys.add(key); - options.setKeys(keys); - } - return true; - } - - public String getMultipartRequestType() { - String accept = connection.getRequestProperty("Accept"); - if(accept.startsWith("multipart/")) { - return accept; - } - return null; - } - - public TDStatus openDB() { - if(db == null) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - if(!db.exists()) { - return new TDStatus(TDStatus.NOT_FOUND); - } - if(!db.open()) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - return new TDStatus(TDStatus.OK); - } - - public static List splitPath(URL url) { - String pathString = url.getPath(); - if(pathString.startsWith("/")) { - pathString = pathString.substring(1); - } - List result = new ArrayList(); - //we want empty string to return empty list - if(pathString.length() == 0) { - return result; - } - for (String component : pathString.split("/")) { - result.add(URLDecoder.decode(component)); - } - return result; - } - - public void sendResponse() { - if(!responseSent) { - responseSent = true; - if(callbackBlock != null) { - callbackBlock.onResponseReady(); - } - } - } - - public void start() { - // Refer to: http://wiki.apache.org/couchdb/Complete_HTTP_API_Reference - - // We're going to map the request into a method call using reflection based on the method and path. - // Accumulate the method name into the string 'message': - String method = connection.getRequestMethod(); - if("HEAD".equals(method)) { - method = "GET"; - } - String message = String.format("do_%s", method); - - // First interpret the components of the request: - List path = splitPath(connection.getURL()); - if(path == null) { - connection.setResponseCode(TDStatus.BAD_REQUEST); - try { - connection.getResponseOutputStream().close(); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "Error closing empty output stream"); - } - sendResponse(); - return; - } - - int pathLen = path.size(); - if(pathLen > 0) { - String dbName = path.get(0); - if(dbName.startsWith("_")) { - message += dbName; // special root path, like /_all_dbs - } else { - message += "_Database"; - db = server.getDatabaseNamed(dbName); - if(db == null) { - connection.setResponseCode(TDStatus.BAD_REQUEST); - try { - connection.getResponseOutputStream().close(); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "Error closing empty output stream"); - } - sendResponse(); - return; - } - } - } else { - message += "Root"; - } - - String docID = null; - if(db != null && pathLen > 1) { - message = message.replaceFirst("_Database", "_Document"); - // Make sure database exists, then interpret doc name: - TDStatus status = openDB(); - if(!status.isSuccessful()) { - connection.setResponseCode(status.getCode()); - try { - connection.getResponseOutputStream().close(); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "Error closing empty output stream"); - } - sendResponse(); - return; - } - String name = path.get(1); - if(!name.startsWith("_")) { - // Regular document - if(!TDDatabase.isValidDocumentId(name)) { - connection.setResponseCode(TDStatus.BAD_REQUEST); - try { - connection.getResponseOutputStream().close(); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "Error closing empty output stream"); - } - sendResponse(); - return; - } - docID = name; - } else if("_design".equals(name) || "_local".equals(name)) { - // "_design/____" and "_local/____" are document names - if(pathLen <= 2) { - connection.setResponseCode(TDStatus.NOT_FOUND); - try { - connection.getResponseOutputStream().close(); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "Error closing empty output stream"); - } - sendResponse(); - return; - } - docID = name + "/" + path.get(2); - path.set(1, docID); - path.remove(2); - pathLen--; - } else if(name.startsWith("_design") || name.startsWith("_local")) { - // This is also a document, just with a URL-encoded "/" - docID = name; - } else { - // Special document name like "_all_docs": - message += name; - if(pathLen > 2) { - List subList = path.subList(2, pathLen-1); - StringBuilder sb = new StringBuilder(); - Iterator iter = subList.iterator(); - while(iter.hasNext()) { - sb.append(iter.next()); - if(iter.hasNext()) { - sb.append("/"); - } - } - docID = sb.toString(); - } - } - } - - String attachmentName = null; - if(docID != null && pathLen > 2) { - message = message.replaceFirst("_Document", "_Attachment"); - // Interpret attachment name: - attachmentName = path.get(2); - if(attachmentName.startsWith("_") && docID.startsWith("_design")) { - // Design-doc attribute like _info or _view - message = message.replaceFirst("_Attachment", "_DesignDocument"); - docID = docID.substring(8); // strip the "_design/" prefix - attachmentName = pathLen > 3 ? path.get(3) : null; - } else { - if (pathLen > 3) { - List subList = path.subList(2, pathLen); - StringBuilder sb = new StringBuilder(); - Iterator iter = subList.iterator(); - while(iter.hasNext()) { - sb.append(iter.next()); - if(iter.hasNext()) { - //sb.append("%2F"); - sb.append("/"); - } - } - attachmentName = sb.toString(); - } - } - } - - //Log.d(TAG, "path: " + path + " message: " + message + " docID: " + docID + " attachmentName: " + attachmentName); - - // Send myself a message based on the components: - TDStatus status = new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - try { - Method m = this.getClass().getMethod(message, TDDatabase.class, String.class, String.class); - status = (TDStatus)m.invoke(this, db, docID, attachmentName); - } catch (NoSuchMethodException msme) { - try { - Method m = this.getClass().getMethod("do_UNKNOWN", TDDatabase.class, String.class, String.class); - status = (TDStatus)m.invoke(this, db, docID, attachmentName); - } catch (Exception e) { - //default status is internal server error - } - } catch (Exception e) { - //default status is internal server error - Log.e(TDDatabase.TAG, "Exception in TDRouter", e); - } - - // Configure response headers: - if(status.isSuccessful() && connection.getResponseBody() == null && connection.getHeaderField("Content-Type") == null) { - connection.setResponseBody(new TDBody("{\"ok\":true}".getBytes())); - } - - if(connection.getResponseBody() != null && connection.getResponseBody().isValidJSON()) { - connection.getResHeader().add("Content-Type", "application/json"); - } - - // Check for a mismatch between the Accept request header and the response type: - String accept = connection.getRequestProperty("Accept"); - if(accept != null && !"*/*".equals(accept)) { - String responseType = connection.getBaseContentType(); - if(responseType != null && accept.indexOf(responseType) < 0) { - Log.e(TDDatabase.TAG, String.format("Error 406: Can't satisfy request Accept: %s", accept)); - status = new TDStatus(TDStatus.NOT_ACCEPTABLE); - } - } - - connection.getResHeader().add("Server", String.format("TouchDB %s", getVersionString())); - - // If response is ready (nonzero status), tell my client about it: - if(status.getCode() != 0) { - connection.setResponseCode(status.getCode()); - - if(connection.getResponseBody() != null) { - ByteArrayInputStream bais = new ByteArrayInputStream(connection.getResponseBody().getJson()); - connection.setResponseInputStream(bais); - } else { - - try { - connection.getResponseOutputStream().close(); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "Error closing empty output stream"); - } - } - sendResponse(); - } - } - - public void stop() { - callbackBlock = null; - if(db != null) { - db.deleteObserver(this); - } - } - - public TDStatus do_UNKNOWN(TDDatabase db, String docID, String attachmentName) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - /*************************************************************************************************/ - /*** TDRouter+Handlers ***/ - /*************************************************************************************************/ - - public void setResponseLocation(URL url) { - String location = url.toExternalForm(); - String query = url.getQuery(); - if(query != null) { - int startOfQuery = location.indexOf(query); - if(startOfQuery > 0) { - location = location.substring(0, startOfQuery); - } - } - connection.getResHeader().add("Location", location); - } - - /** SERVER REQUESTS: **/ - - public TDStatus do_GETRoot(TDDatabase _db, String _docID, String _attachmentName) { - Map info = new HashMap(); - info.put("TouchDB", "Welcome"); - info.put("couchdb", "Welcome"); // for compatibility - info.put("version", getVersionString()); - connection.setResponseBody(new TDBody(info)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_GET_all_dbs(TDDatabase _db, String _docID, String _attachmentName) { - List dbs = server.allDatabaseNames(); - connection.setResponseBody(new TDBody(dbs)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_GET_session(TDDatabase _db, String _docID, String _attachmentName) { - // Send back an "Admin Party"-like response - Map session= new HashMap(); - Map userCtx = new HashMap(); - String[] roles = {"_admin"}; - session.put("ok", true); - userCtx.put("name", null); - userCtx.put("roles", roles); - session.put("userCtx", userCtx); - connection.setResponseBody(new TDBody(session)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_POST_replicate(TDDatabase _db, String _docID, String _attachmentName) { - // Extract the parameters from the JSON request body: - // http://wiki.apache.org/couchdb/Replication - Map body = getBodyAsDictionary(); - if(body == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - String source = (String)body.get("source"); - String target = (String)body.get("target"); - Boolean createTargetBoolean = (Boolean)body.get("create_target"); - boolean createTarget = (createTargetBoolean != null && createTargetBoolean.booleanValue()); - Boolean continuousBoolean = (Boolean)body.get("continuous"); - boolean continuous = (continuousBoolean != null && continuousBoolean.booleanValue()); - Boolean cancelBoolean = (Boolean)body.get("cancel"); - boolean cancel = (cancelBoolean != null && cancelBoolean.booleanValue()); - String access_token = (String) ((Map)body.get("query_params")).get("access_token"); - - // Map the 'source' and 'target' JSON params to a local database and remote URL: - if(source == null || target == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - boolean push = false; - TDDatabase db = server.getExistingDatabaseNamed(source); - String remoteStr = null; - if(db != null) { - remoteStr = target; - push = true; - } else { - remoteStr = source; - if(createTarget && !cancel) { - db = server.getDatabaseNamed(target); - if(!db.open()) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - } else { - db = server.getExistingDatabaseNamed(target); - } - if(db == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - } - - URL remote = null; - try { - remote = new URL(remoteStr); - } catch (MalformedURLException e) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - if(remote == null || !remote.getProtocol().startsWith("http")) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - if(!cancel) { - // Start replication: - TDReplicator repl = db.getReplicator(remote, server.getDefaultHttpClientFactory(), push, access_token, continuous, server.getWorkExecutor()); - if(repl == null) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - String filterName = (String)body.get("filter"); - if(filterName != null) { - repl.setFilterName(filterName); - Map filterParams = (Map)body.get("query_params"); - if(filterParams != null) { - repl.setFilterParams(filterParams); - } - } - - if(push) { - ((TDPusher)repl).setCreateTarget(createTarget); - } - repl.start(); - Map result = new HashMap(); - result.put("session_id", repl.getSessionID()); - connection.setResponseBody(new TDBody(result)); - } else { - // Cancel replication: - TDReplicator repl = db.getActiveReplicator(remote, push); - if(repl == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - repl.stop(); - } - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_GET_uuids(TDDatabase _db, String _docID, String _attachmentName) { - int count = Math.min(1000, getIntQuery("count", 1)); - List uuids = new ArrayList(count); - for(int i=0; i result = new HashMap(); - result.put("uuids", uuids); - connection.setResponseBody(new TDBody(result)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_GET_active_tasks(TDDatabase _db, String _docID, String _attachmentName) { - // http://wiki.apache.org/couchdb/HttpGetActiveTasks - List> activities = new ArrayList>(); - for (TDDatabase db : server.allOpenDatabases()) { - List activeReplicators = db.getActiveReplicators(); - if(activeReplicators != null) { - for (TDReplicator replicator : activeReplicators) { - String source = replicator.getRemote().toExternalForm(); - String target = db.getName(); - if(replicator.isPush()) { - String tmp = source; - source = target; - target = tmp; - } - int processed = replicator.getChangesProcessed(); - int total = replicator.getChangesTotal(); - String status = String.format("Processed %d / %d changes", processed, total); - int progress = (total > 0) ? Math.round(100 * processed / (float)total) : 0; - Map activity = new HashMap(); - activity.put("type", "Replication"); - activity.put("task", replicator.getSessionID()); - activity.put("source", source); - activity.put("target", target); - activity.put("status", status); - activity.put("progress", progress); - activities.add(activity); - } - } - } - connection.setResponseBody(new TDBody(activities)); - return new TDStatus(TDStatus.OK); - } - - /** DATABASE REQUESTS: **/ - - public TDStatus do_GET_Database(TDDatabase _db, String _docID, String _attachmentName) { - // http://wiki.apache.org/couchdb/HTTP_database_API#Database_Information - TDStatus status = openDB(); - if(!status.isSuccessful()) { - return status; - } - int num_docs = db.getDocumentCount(); - long update_seq = db.getLastSequence(); - Map result = new HashMap(); - result.put("db_name", db.getName()); - result.put("db_uuid", db.publicUUID()); - result.put("doc_count", num_docs); - result.put("update_seq", update_seq); - result.put("disk_size", db.totalDataSize()); - connection.setResponseBody(new TDBody(result)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_PUT_Database(TDDatabase _db, String _docID, String _attachmentName) { - if(db.exists()) { - return new TDStatus(TDStatus.PRECONDITION_FAILED); - } - if(!db.open()) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - setResponseLocation(connection.getURL()); - return new TDStatus(TDStatus.CREATED); - } - - public TDStatus do_DELETE_Database(TDDatabase _db, String _docID, String _attachmentName) { - if(getQuery("rev") != null) { - return new TDStatus(TDStatus.BAD_REQUEST); // CouchDB checks for this; probably meant to be a document deletion - } - return server.deleteDatabaseNamed(db.getName()) ? new TDStatus(TDStatus.OK) : new TDStatus(TDStatus.NOT_FOUND); - } - - public TDStatus do_POST_Database(TDDatabase _db, String _docID, String _attachmentName) { - TDStatus status = openDB(); - if(!status.isSuccessful()) { - return status; - } - return update(db, null, getBodyAsDictionary(), false); - } - - public TDStatus do_GET_Document_all_docs(TDDatabase _db, String _docID, String _attachmentName) { - TDQueryOptions options = new TDQueryOptions(); - if(!getQueryOptions(options)) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - Map result = db.getAllDocs(options); - if(result == null) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - connection.setResponseBody(new TDBody(result)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_POST_Document_all_docs(TDDatabase _db, String _docID, String _attachmentName) { - TDQueryOptions options = new TDQueryOptions(); - if (!getQueryOptions(options)) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - Map body = getBodyAsDictionary(); - if (body == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - Map result = null; - if (body.containsKey("keys") && body.get("keys") instanceof ArrayList) { - ArrayList keys = (ArrayList) body.get("keys"); - result = db.getDocsWithIDs(keys, options); - } else { - result = db.getAllDocs(options); - } - - if (result == null) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - connection.setResponseBody(new TDBody(result)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_POST_Document_bulk_docs(TDDatabase _db, String _docID, String _attachmentName) { - Map bodyDict = getBodyAsDictionary(); - if(bodyDict == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - List> docs = (List>) bodyDict.get("docs"); - - boolean allObj = false; - if(getQuery("all_or_nothing") == null || (getQuery("all_or_nothing") != null && (new Boolean(getQuery("all_or_nothing"))))) { - allObj = true; - } - // allowConflict If false, an error status 409 will be returned if the insertion would create a conflict, i.e. if the previous revision already has a child. - boolean allOrNothing = (allObj && allObj != false); - boolean noNewEdits = true; - if(getQuery("new_edits") == null || (getQuery("new_edits") != null && (new Boolean(getQuery("new_edits"))))) { - noNewEdits = false; - } - boolean ok = false; - db.beginTransaction(); - List> results = new ArrayList>(); - try { - for (Map doc : docs) { - String docID = (String) doc.get("_id"); - TDRevision rev = null; - TDStatus status = new TDStatus(TDStatus.BAD_REQUEST); - TDBody docBody = new TDBody(doc); - if (noNewEdits) { - rev = new TDRevision(docBody); - if(rev.getRevId() == null || rev.getDocId() == null || !rev.getDocId().equals(docID)) { - status = new TDStatus(TDStatus.BAD_REQUEST); - } else { - List history = TDDatabase.parseCouchDBRevisionHistory(doc); - status = db.forceInsert(rev, history, null); - } - } else { - TDStatus outStatus = new TDStatus(); - rev = update(db, docID, docBody, false, allOrNothing, outStatus); - status.setCode(outStatus.getCode()); - } - Map result = null; - if(status.isSuccessful()) { - result = new HashMap(); - result.put("ok", true); - result.put("id", docID); - if (rev != null) { - result.put("rev", rev.getRevId()); - } - } else if(allOrNothing) { - return status; // all_or_nothing backs out if there's any error - } else if(status.getCode() == TDStatus.FORBIDDEN) { - result = new HashMap(); - result.put("error", "validation failed"); - result.put("id", docID); - } else if(status.getCode() == TDStatus.CONFLICT) { - result = new HashMap(); - result.put("error", "conflict"); - result.put("id", docID); - } else { - return status; // abort the whole thing if something goes badly wrong - } - if(result != null) { - results.add(result); - } - } - Log.w(TDDatabase.TAG, String.format("%s finished inserting %d revisions in bulk", this, docs.size())); - ok = true; - } catch (Exception e) { - Log.w(TDDatabase.TAG, String.format("%s: Exception inserting revisions in bulk", this), e); - } finally { - db.endTransaction(ok); - } - Log.d(TDDatabase.TAG, "results: " + results.toString()); - connection.setResponseBody(new TDBody(results)); - return new TDStatus(TDStatus.CREATED); - } - - public TDStatus do_POST_Document_revs_diff(TDDatabase _db, String _docID, String _attachmentName) { - // http://wiki.apache.org/couchdb/HttpPostRevsDiff - // Collect all of the input doc/revision IDs as TDRevisions: - TDRevisionList revs = new TDRevisionList(); - Map body = getBodyAsDictionary(); - if(body == null) { - return new TDStatus(TDStatus.BAD_JSON); - } - for (String docID : body.keySet()) { - List revIDs = (List)body.get(docID); - for (String revID : revIDs) { - TDRevision rev = new TDRevision(docID, revID, false); - revs.add(rev); - } - } - - // Look them up, removing the existing ones from revs: - if(!db.findMissingRevisions(revs)) { - return new TDStatus(TDStatus.DB_ERROR); - } - - // Return the missing revs in a somewhat different format: - Map diffs = new HashMap(); - for (TDRevision rev : revs) { - String docID = rev.getDocId(); - - List missingRevs = null; - Map idObj = (Map)diffs.get(docID); - if(idObj != null) { - missingRevs = (List)idObj.get("missing"); - } else { - idObj = new HashMap(); - } - - if(missingRevs == null) { - missingRevs = new ArrayList(); - idObj.put("missing", missingRevs); - diffs.put(docID, idObj); - } - missingRevs.add(rev.getRevId()); - } - - // FIXME add support for possible_ancestors - - connection.setResponseBody(new TDBody(diffs)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_POST_Document_compact(TDDatabase _db, String _docID, String _attachmentName) { - TDStatus status = _db.compact(); - if (status.getCode() < 300) { - TDStatus outStatus = new TDStatus(); - outStatus.setCode(202); // CouchDB returns 202 'cause it's an async operation - return outStatus; - } else { - return status; - } - } - - public TDStatus do_POST_Document_ensure_full_commit(TDDatabase _db, String _docID, String _attachmentName) { - return new TDStatus(TDStatus.OK); - } - - /** CHANGES: **/ - - public Map changesDictForRevision(TDRevision rev) { - Map changesDict = new HashMap(); - changesDict.put("rev", rev.getRevId()); - - List> changes = new ArrayList>(); - changes.add(changesDict); - - Map result = new HashMap(); - result.put("seq", rev.getSequence()); - result.put("id", rev.getDocId()); - result.put("changes", changes); - if(rev.isDeleted()) { - result.put("deleted", true); - } - if(changesIncludesDocs) { - result.put("doc", rev.getProperties()); - } - return result; - } - - public Map responseBodyForChanges(List changes, long since) { - List> results = new ArrayList>(); - for (TDRevision rev : changes) { - Map changeDict = changesDictForRevision(rev); - results.add(changeDict); - } - if(changes.size() > 0) { - since = changes.get(changes.size() - 1).getSequence(); - } - Map result = new HashMap(); - result.put("results", results); - result.put("last_seq", since); - return result; - } - - public Map responseBodyForChangesWithConflicts(List changes, long since) { - // Assumes the changes are grouped by docID so that conflicts will be adjacent. - List> entries = new ArrayList>(); - String lastDocID = null; - Map lastEntry = null; - for (TDRevision rev : changes) { - String docID = rev.getDocId(); - if(docID.equals(lastDocID)) { - Map changesDict = new HashMap(); - changesDict.put("rev", rev.getRevId()); - List> inchanges = (List>)lastEntry.get("changes"); - inchanges.add(changesDict); - } else { - lastEntry = changesDictForRevision(rev); - entries.add(lastEntry); - lastDocID = docID; - } - } - // After collecting revisions, sort by sequence: - Collections.sort(entries, new Comparator>() { - public int compare(Map e1, Map e2) { - return TDMisc.TDSequenceCompare((Long)e1.get("seq"), (Long)e2.get("seq")); - } - }); - - Long lastSeq = (Long)entries.get(entries.size() - 1).get("seq"); - if(lastSeq == null) { - lastSeq = since; - } - - Map result = new HashMap(); - result.put("results", entries); - result.put("last_seq", lastSeq); - return result; - } - - public void sendContinuousChange(TDRevision rev) { - Map changeDict = changesDictForRevision(rev); - try { - String jsonString = TDServer.getObjectMapper().writeValueAsString(changeDict); - if(callbackBlock != null) { - byte[] json = (jsonString + "\n").getBytes(); - OutputStream os = connection.getResponseOutputStream(); - try { - os.write(json); - os.flush(); - } catch (Exception e) { - Log.e(TDDatabase.TAG, "IOException writing to internal streams", e); - } - } - } catch (Exception e) { - Log.w("Unable to serialize change to JSON", e); - } - } - - @Override - public void update(Observable observable, Object changeObject) { - if(observable == db) { - //make sure we're listening to the right events - Map changeNotification = (Map)changeObject; - - TDRevision rev = (TDRevision)changeNotification.get("rev"); - - if(changesFilter != null && !changesFilter.filter(rev)) { - return; - } - - if(longpoll) { - Log.w(TDDatabase.TAG, "TDRouter: Sending longpoll response"); - sendResponse(); - List revs = new ArrayList(); - revs.add(rev); - Map body = responseBodyForChanges(revs, 0); - if(callbackBlock != null) { - byte[] data = null; - try { - data = TDServer.getObjectMapper().writeValueAsBytes(body); - } catch (Exception e) { - Log.w(TDDatabase.TAG, "Error serializing JSON", e); - } - OutputStream os = connection.getResponseOutputStream(); - try { - os.write(data); - os.close(); - } catch (IOException e) { - Log.e(TDDatabase.TAG, "IOException writing to internal streams", e); - } - } - } else { - Log.w(TDDatabase.TAG, "TDRouter: Sending continous change chunk"); - sendContinuousChange(rev); - } - - } - - } - - public TDStatus do_GET_Document_changes(TDDatabase _db, String docID, String _attachmentName) { - // http://wiki.apache.org/couchdb/HTTP_database_API#Changes - TDChangesOptions options = new TDChangesOptions(); - changesIncludesDocs = getBooleanQuery("include_docs"); - options.setIncludeDocs(changesIncludesDocs); - String style = getQuery("style"); - if(style != null && style.equals("all_docs")) { - options.setIncludeConflicts(true); - } - options.setContentOptions(getContentOptions()); - options.setSortBySequence(!options.isIncludeConflicts()); - options.setLimit(getIntQuery("limit", options.getLimit())); - - int since = getIntQuery("since", 0); - - String filterName = getQuery("filter"); - if(filterName != null) { - changesFilter = db.getFilterNamed(filterName); - if(changesFilter == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - } - - TDRevisionList changes = db.changesSince(since, options, changesFilter); - - if(changes == null) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - - String feed = getQuery("feed"); - longpoll = "longpoll".equals(feed); - boolean continuous = !longpoll && "continuous".equals(feed); - - if(continuous || (longpoll && changes.size() == 0)) { - connection.setChunked(true); - connection.setResponseCode(TDStatus.OK); - sendResponse(); - if(continuous) { - for (TDRevision rev : changes) { - sendContinuousChange(rev); - } - } - db.addObserver(this); - // Don't close connection; more data to come - return new TDStatus(0); - } else { - if(options.isIncludeConflicts()) { - connection.setResponseBody(new TDBody(responseBodyForChangesWithConflicts(changes, since))); - } else { - connection.setResponseBody(new TDBody(responseBodyForChanges(changes, since))); - } - return new TDStatus(TDStatus.OK); - } - } - - /** DOCUMENT REQUESTS: **/ - - public String getRevIDFromIfMatchHeader() { - String ifMatch = connection.getRequestProperty("If-Match"); - if(ifMatch == null) { - return null; - } - // Value of If-Match is an ETag, so have to trim the quotes around it: - if(ifMatch.length() > 2 && ifMatch.startsWith("\"") && ifMatch.endsWith("\"")) { - return ifMatch.substring(1,ifMatch.length() - 2); - } else { - return null; - } - } - - public String setResponseEtag(TDRevision rev) { - String eTag = String.format("\"%s\"", rev.getRevId()); - connection.getResHeader().add("Etag", eTag); - return eTag; - } - - public TDStatus do_GET_Document(TDDatabase _db, String docID, String _attachmentName) { - // http://wiki.apache.org/couchdb/HTTP_Document_API#GET - boolean isLocalDoc = docID.startsWith("_local"); - EnumSet options = getContentOptions(); - String openRevsParam = getQuery("open_revs"); - if(openRevsParam == null || isLocalDoc) { - // Regular GET: - String revID = getQuery("rev"); // often null - TDRevision rev = null; - if(isLocalDoc) { - rev = db.getLocalDocument(docID, revID); - } else { - rev = db.getDocumentWithIDAndRev(docID, revID, options); - // Handle ?atts_since query by stubbing out older attachments: - //?atts_since parameter - value is a (URL-encoded) JSON array of one or more revision IDs. - // The response will include the content of only those attachments that changed since the given revision(s). - //(You can ask for this either in the default JSON or as multipart/related, as previously described.) - List attsSince = (List)getJSONQuery("atts_since"); - if (attsSince != null) { - String ancestorId = db.findCommonAncestorOf(rev, attsSince); - if (ancestorId != null) { - int generation = TDRevision.generationFromRevID(ancestorId); - db.stubOutAttachmentsIn(rev, generation + 1); - } - } - } - if(rev == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - if(cacheWithEtag(rev.getRevId())) { - return new TDStatus(TDStatus.NOT_MODIFIED); // set ETag and check conditional GET - } - - connection.setResponseBody(rev.getBody()); - } else { - List> result = null; - if(openRevsParam.equals("all")) { - // Get all conflicting revisions: - TDRevisionList allRevs = db.getAllRevisionsOfDocumentID(docID, true); - result = new ArrayList>(allRevs.size()); - for (TDRevision rev : allRevs) { - TDStatus status = db.loadRevisionBody(rev, options); - if(status.isSuccessful()) { - Map dict = new HashMap(); - dict.put("ok", rev.getProperties()); - result.add(dict); - } else if(status.getCode() != TDStatus.INTERNAL_SERVER_ERROR) { - Map dict = new HashMap(); - dict.put("missing", rev.getRevId()); - result.add(dict); - } else { - return status; // internal error getting revision - } - } - } else { - // ?open_revs=[...] returns an array of revisions of the document: - List openRevs = (List)getJSONQuery("open_revs"); - if(openRevs == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - result = new ArrayList>(openRevs.size()); - for (String revID : openRevs) { - TDRevision rev = db.getDocumentWithIDAndRev(docID, revID, options); - if(rev != null) { - Map dict = new HashMap(); - dict.put("ok", rev.getProperties()); - result.add(dict); - } else { - Map dict = new HashMap(); - dict.put("missing", revID); - result.add(dict); - } - } - } - String acceptMultipart = getMultipartRequestType(); - if(acceptMultipart != null) { - //FIXME figure out support for multipart - throw new UnsupportedOperationException(); - } else { - connection.setResponseBody(new TDBody(result)); - } - } - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_GET_Attachment(TDDatabase _db, String docID, String _attachmentName) { - // http://wiki.apache.org/couchdb/HTTP_Document_API#GET - EnumSet options = getContentOptions(); - options.add(TDContentOptions.TDNoBody); - String revID = getQuery("rev"); // often null - TDRevision rev = db.getDocumentWithIDAndRev(docID, revID, options); - if(rev == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - if(cacheWithEtag(rev.getRevId())) { - return new TDStatus(TDStatus.NOT_MODIFIED); // set ETag and check conditional GET - } - - String type = null; - TDStatus status = new TDStatus(); - String acceptEncoding = connection.getRequestProperty("Accept-Encoding"); - TDAttachment contents = db.getAttachmentForSequence(rev.getSequence(), _attachmentName, status); - - if (contents == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - type = contents.getContentType(); - if (type != null) { - connection.getResHeader().add("Content-Type", type); - } - if (acceptEncoding != null && acceptEncoding.equals("gzip")) { - connection.getResHeader().add("Content-Encoding", acceptEncoding); - } - - connection.setResponseInputStream(contents.getContentStream()); - return new TDStatus(TDStatus.OK); - } - - /** - * NOTE this departs from the iOS version, returning revision, passing status back by reference - */ - public TDRevision update(TDDatabase _db, String docID, TDBody body, boolean deleting, boolean allowConflict, TDStatus outStatus) { - boolean isLocalDoc = docID != null && docID.startsWith(("_local")); - String prevRevID = null; - - if(!deleting) { - Boolean deletingBoolean = (Boolean)body.getPropertyForKey("deleted"); - deleting = (deletingBoolean != null && deletingBoolean.booleanValue()); - if(docID == null) { - if(isLocalDoc) { - outStatus.setCode(TDStatus.METHOD_NOT_ALLOWED); - return null; - } - // POST's doc ID may come from the _id field of the JSON body, else generate a random one. - docID = (String)body.getPropertyForKey("_id"); - if(docID == null) { - if(deleting) { - outStatus.setCode(TDStatus.BAD_REQUEST); - return null; - } - docID = TDDatabase.generateDocumentId(); - } - } - // PUT's revision ID comes from the JSON body. - prevRevID = (String)body.getPropertyForKey("_rev"); - } else { - // DELETE's revision ID comes from the ?rev= query param - prevRevID = getQuery("rev"); - } - - // A backup source of revision ID is an If-Match header: - if(prevRevID == null) { - prevRevID = getRevIDFromIfMatchHeader(); - } - - TDRevision rev = new TDRevision(docID, null, deleting); - rev.setBody(body); - - TDRevision result = null; - TDStatus tmpStatus = new TDStatus(); - if(isLocalDoc) { - result = _db.putLocalRevision(rev, prevRevID, tmpStatus); - } else { - result = _db.putRevision(rev, prevRevID, allowConflict, tmpStatus); - } - outStatus.setCode(tmpStatus.getCode()); - return result; - } - - public TDStatus update(TDDatabase _db, String docID, Map bodyDict, boolean deleting) { - TDBody body = new TDBody(bodyDict); - TDStatus status = new TDStatus(); - TDRevision rev = update(_db, docID, body, deleting, false, status); - if(status.isSuccessful()) { - cacheWithEtag(rev.getRevId()); // set ETag - if(!deleting) { - URL url = connection.getURL(); - String urlString = url.toExternalForm(); - if(docID != null) { - urlString += "/" + rev.getDocId(); - try { - url = new URL(urlString); - } catch (MalformedURLException e) { - Log.w("Malformed URL", e); - } - } - setResponseLocation(url); - } - Map result = new HashMap(); - result.put("ok", true); - result.put("id", rev.getDocId()); - result.put("rev", rev.getRevId()); - connection.setResponseBody(new TDBody(result)); - } - return status; - } - - public TDStatus do_PUT_Document(TDDatabase _db, String docID, String _attachmentName) { - Map bodyDict = getBodyAsDictionary(); - if(bodyDict == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - - if(getQuery("new_edits") == null || (getQuery("new_edits") != null && (new Boolean(getQuery("new_edits"))))) { - // Regular PUT - return update(_db, docID, bodyDict, false); - } else { - // PUT with new_edits=false -- forcible insertion of existing revision: - TDBody body = new TDBody(bodyDict); - TDRevision rev = new TDRevision(body); - if(rev.getRevId() == null || rev.getDocId() == null || !rev.getDocId().equals(docID)) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - List history = TDDatabase.parseCouchDBRevisionHistory(body.getProperties()); - return db.forceInsert(rev, history, null); - } - } - - public TDStatus do_DELETE_Document(TDDatabase _db, String docID, String _attachmentName) { - return update(_db, docID, null, true); - } - - public TDStatus updateAttachment(String attachment, String docID, InputStream contentStream) { - TDStatus status = new TDStatus(); - String revID = getQuery("rev"); - if(revID == null) { - revID = getRevIDFromIfMatchHeader(); - } - TDRevision rev = db.updateAttachment(attachment, contentStream, connection.getRequestProperty("content-type"), - docID, revID, status); - if(status.isSuccessful()) { - Map resultDict = new HashMap(); - resultDict.put("ok", true); - resultDict.put("id", rev.getDocId()); - resultDict.put("rev", rev.getRevId()); - connection.setResponseBody(new TDBody(resultDict)); - cacheWithEtag(rev.getRevId()); - if(contentStream != null) { - setResponseLocation(connection.getURL()); - } - } - return status; - } - - public TDStatus do_PUT_Attachment(TDDatabase _db, String docID, String _attachmentName) { - return updateAttachment(_attachmentName, docID, connection.getRequestInputStream()); - } - - public TDStatus do_DELETE_Attachment(TDDatabase _db, String docID, String _attachmentName) { - return updateAttachment(_attachmentName, docID, null); - } - - /** VIEW QUERIES: **/ - - public TDView compileView(String viewName, Map viewProps) { - String language = (String)viewProps.get("language"); - if(language == null) { - language = "javascript"; - } - String mapSource = (String)viewProps.get("map"); - if(mapSource == null) { - return null; - } - TDViewMapBlock mapBlock = TDView.getCompiler().compileMapFunction(mapSource, language); - if(mapBlock == null) { - Log.w(TDDatabase.TAG, String.format("View %s has unknown map function: %s", viewName, mapSource)); - return null; - } - String reduceSource = (String)viewProps.get("reduce"); - TDViewReduceBlock reduceBlock = null; - if(reduceSource != null) { - reduceBlock = TDView.getCompiler().compileReduceFunction(reduceSource, language); - if(reduceBlock == null) { - Log.w(TDDatabase.TAG, String.format("View %s has unknown reduce function: %s", viewName, reduceBlock)); - return null; - } - } - - TDView view = db.getViewNamed(viewName); - view.setMapReduceBlocks(mapBlock, reduceBlock, "1"); - String collation = (String)viewProps.get("collation"); - if("raw".equals(collation)) { - view.setCollation(TDViewCollation.TDViewCollationRaw); - } - return view; - } - - public TDStatus queryDesignDoc(String designDoc, String viewName, List keys) { - String tdViewName = String.format("%s/%s", designDoc, viewName); - TDView view = db.getExistingViewNamed(tdViewName); - if(view == null || view.getMapBlock() == null) { - // No TouchDB view is defined, or it hasn't had a map block assigned; - // see if there's a CouchDB view definition we can compile: - TDRevision rev = db.getDocumentWithIDAndRev(String.format("_design/%s", designDoc), null, EnumSet.noneOf(TDContentOptions.class)); - if(rev == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - Map views = (Map)rev.getProperties().get("views"); - Map viewProps = (Map)views.get(viewName); - if(viewProps == null) { - return new TDStatus(TDStatus.NOT_FOUND); - } - // If there is a CouchDB view, see if it can be compiled from source: - view = compileView(tdViewName, viewProps); - if(view == null) { - return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); - } - } - - TDQueryOptions options = new TDQueryOptions(); - - //if the view contains a reduce block, it should default to reduce=true - if(view.getReduceBlock() != null) { - options.setReduce(true); - } - - if(!getQueryOptions(options)) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - if(keys != null) { - options.setKeys(keys); - } - - TDStatus status = view.updateIndex(); - if(!status.isSuccessful()) { - return status; - } - - long lastSequenceIndexed = view.getLastSequenceIndexed(); - - // Check for conditional GET and set response Etag header: - if(keys == null) { - long eTag = options.isIncludeDocs() ? db.getLastSequence() : lastSequenceIndexed; - if(cacheWithEtag(String.format("%d", eTag))) { - return new TDStatus(TDStatus.NOT_MODIFIED); - } - } - - List> rows = view.queryWithOptions(options, status); - if(rows == null) { - return status; - } - - Map responseBody = new HashMap(); - responseBody.put("rows", rows); - responseBody.put("total_rows", rows.size()); - responseBody.put("offset", options.getSkip()); - if(options.isUpdateSeq()) { - responseBody.put("update_seq", lastSequenceIndexed); - } - connection.setResponseBody(new TDBody(responseBody)); - return new TDStatus(TDStatus.OK); - } - - public TDStatus do_GET_DesignDocument(TDDatabase _db, String designDocID, String viewName) { - return queryDesignDoc(designDocID, viewName, null); - } - - public TDStatus do_POST_DesignDocument(TDDatabase _db, String designDocID, String viewName) { - Map bodyDict = getBodyAsDictionary(); - if(bodyDict == null) { - return new TDStatus(TDStatus.BAD_REQUEST); - } - List keys = (List) bodyDict.get("keys"); - return queryDesignDoc(designDocID, viewName, keys); - } - - @Override - public String toString() { - String url = "Unknown"; - if(connection != null && connection.getURL() != null) { - url = connection.getURL().toExternalForm(); - } - return String.format("TDRouter [%s]", url); - } + private TDServer server; + private TDDatabase db; + private TDURLConnection connection; + private Map queries; + private boolean changesIncludesDocs = false; + private TDRouterCallbackBlock callbackBlock; + private boolean responseSent = false; + private boolean waiting = false; + private TDFilterBlock changesFilter; + private boolean longpoll = false; + + public static String getVersionString() { + return TouchDBVersion.TouchDBVersionNumber; + } + + public TDRouter(TDServer server, TDURLConnection connection) { + this.server = server; + this.connection = connection; + } + + public void setCallbackBlock(TDRouterCallbackBlock callbackBlock) { + this.callbackBlock = callbackBlock; + } + + public Map getQueries() { + if (queries == null) { + String queryString = connection.getURL().getQuery(); + if (queryString != null && queryString.length() > 0) { + queries = new HashMap(); + for (String component : queryString.split("&")) { + int location = component.indexOf('='); + if (location > 0) { + String key = component.substring(0, location); + String value = component.substring(location + 1); + queries.put(key, value); + } + } + + } + } + return queries; + } + + public String getQuery(String param) { + Map queries = getQueries(); + if (queries != null) { + String value = queries.get(param); + if (value != null) { + return URLDecoder.decode(value); + } + } + return null; + } + + public boolean getBooleanQuery(String param) { + String value = getQuery(param); + return (value != null) && !"false".equals(value) && !"0".equals(value); + } + + public int getIntQuery(String param, int defaultValue) { + int result = defaultValue; + String value = getQuery(param); + if (value != null) { + try { + result = Integer.parseInt(value); + } catch (NumberFormatException e) { + // ignore, will return default value + } + } + + return result; + } + + public Object getJSONQuery(String param) { + String value = getQuery(param); + if (value == null) { + return null; + } + Object result = null; + try { + result = TDServer.getObjectMapper().readValue(value, Object.class); + } catch (Exception e) { + Log.w("Unable to parse JSON Query", e); + } + return result; + } + + public boolean cacheWithEtag(String etag) { + String eTag = String.format("\"%s\"", etag); + connection.getResHeader().add("Etag", eTag); + String requestIfNoneMatch = connection + .getRequestProperty("If-None-Match"); + return eTag.equals(requestIfNoneMatch); + } + + public Map getBodyAsDictionary() { + try { + InputStream contentStream = connection.getRequestInputStream(); + Map bodyMap = TDServer.getObjectMapper().readValue( + contentStream, Map.class); + return bodyMap; + } catch (IOException e) { + return null; + } + } + + public EnumSet getContentOptions() { + EnumSet result = EnumSet + .noneOf(TDContentOptions.class); + if (getBooleanQuery("attachments")) { + result.add(TDContentOptions.TDIncludeAttachments); + } + if (getBooleanQuery("local_seq")) { + result.add(TDContentOptions.TDIncludeLocalSeq); + } + if (getBooleanQuery("conflicts")) { + result.add(TDContentOptions.TDIncludeConflicts); + } + if (getBooleanQuery("revs")) { + result.add(TDContentOptions.TDIncludeRevs); + } + if (getBooleanQuery("revs_info")) { + result.add(TDContentOptions.TDIncludeRevsInfo); + } + return result; + } + + public boolean getQueryOptions(TDQueryOptions options) { + // http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options + options.setSkip(getIntQuery("skip", options.getSkip())); + options.setLimit(getIntQuery("limit", options.getLimit())); + options.setGroupLevel(getIntQuery("group_level", + options.getGroupLevel())); + options.setDescending(getBooleanQuery("descending")); + options.setIncludeDocs(getBooleanQuery("include_docs")); + options.setUpdateSeq(getBooleanQuery("update_seq")); + if (getQuery("inclusive_end") != null) { + options.setInclusiveEnd(getBooleanQuery("inclusive_end")); + } + if (getQuery("reduce") != null) { + options.setReduce(getBooleanQuery("reduce")); + } + options.setGroup(getBooleanQuery("group")); + options.setContentOptions(getContentOptions()); + options.setStartKey(getJSONQuery("startkey")); + options.setEndKey(getJSONQuery("endkey")); + Object key = getJSONQuery("key"); + if (key != null) { + List keys = new ArrayList(); + keys.add(key); + options.setKeys(keys); + } + return true; + } + + public String getMultipartRequestType() { + String accept = connection.getRequestProperty("Accept"); + if (accept.startsWith("multipart/")) { + return accept; + } + return null; + } + + public TDStatus openDB() { + if (db == null) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + if (!db.exists()) { + return new TDStatus(TDStatus.NOT_FOUND); + } + if (!db.open()) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + return new TDStatus(TDStatus.OK); + } + + public static List splitPath(URL url) { + String pathString = url.getPath(); + if (pathString.startsWith("/")) { + pathString = pathString.substring(1); + } + List result = new ArrayList(); + // we want empty string to return empty list + if (pathString.length() == 0) { + return result; + } + for (String component : pathString.split("/")) { + result.add(URLDecoder.decode(component)); + } + return result; + } + + public void sendResponse() { + if (!responseSent) { + responseSent = true; + if (callbackBlock != null) { + callbackBlock.onResponseReady(); + } + } + } + + public void start() { + // Refer to: http://wiki.apache.org/couchdb/Complete_HTTP_API_Reference + + // We're going to map the request into a method call using reflection + // based on the method and path. + // Accumulate the method name into the string 'message': + String method = connection.getRequestMethod(); + if ("HEAD".equals(method)) { + method = "GET"; + } + String message = String.format("do_%s", method); + + // First interpret the components of the request: + List path = splitPath(connection.getURL()); + if (path == null) { + connection.setResponseCode(TDStatus.BAD_REQUEST); + try { + connection.getResponseOutputStream().close(); + } catch (IOException e) { + Log.e(TDDatabase.TAG, "Error closing empty output stream"); + } + sendResponse(); + return; + } + + int pathLen = path.size(); + if (pathLen > 0) { + String dbName = path.get(0); + if (dbName.startsWith("_")) { + message += dbName; // special root path, like /_all_dbs + } else { + message += "_Database"; + db = server.getDatabaseNamed(dbName); + if (db == null) { + connection.setResponseCode(TDStatus.BAD_REQUEST); + try { + connection.getResponseOutputStream().close(); + } catch (IOException e) { + Log.e(TDDatabase.TAG, + "Error closing empty output stream"); + } + sendResponse(); + return; + } + } + } else { + message += "Root"; + } + + String docID = null; + if (db != null && pathLen > 1) { + message = message.replaceFirst("_Database", "_Document"); + // Make sure database exists, then interpret doc name: + TDStatus status = openDB(); + if (!status.isSuccessful()) { + connection.setResponseCode(status.getCode()); + try { + connection.getResponseOutputStream().close(); + } catch (IOException e) { + Log.e(TDDatabase.TAG, "Error closing empty output stream"); + } + sendResponse(); + return; + } + String name = path.get(1); + if (!name.startsWith("_")) { + // Regular document + if (!TDDatabase.isValidDocumentId(name)) { + connection.setResponseCode(TDStatus.BAD_REQUEST); + try { + connection.getResponseOutputStream().close(); + } catch (IOException e) { + Log.e(TDDatabase.TAG, + "Error closing empty output stream"); + } + sendResponse(); + return; + } + docID = name; + } else if ("_design".equals(name) || "_local".equals(name)) { + // "_design/____" and "_local/____" are document names + if (pathLen <= 2) { + connection.setResponseCode(TDStatus.NOT_FOUND); + try { + connection.getResponseOutputStream().close(); + } catch (IOException e) { + Log.e(TDDatabase.TAG, + "Error closing empty output stream"); + } + sendResponse(); + return; + } + docID = name + "/" + path.get(2); + path.set(1, docID); + path.remove(2); + pathLen--; + } else if (name.startsWith("_design") || name.startsWith("_local")) { + // This is also a document, just with a URL-encoded "/" + docID = name; + } else { + // Special document name like "_all_docs": + message += name; + if (pathLen > 2) { + List subList = path.subList(2, pathLen - 1); + StringBuilder sb = new StringBuilder(); + Iterator iter = subList.iterator(); + while (iter.hasNext()) { + sb.append(iter.next()); + if (iter.hasNext()) { + sb.append("/"); + } + } + docID = sb.toString(); + } + } + } + + String attachmentName = null; + if (docID != null && pathLen > 2) { + message = message.replaceFirst("_Document", "_Attachment"); + // Interpret attachment name: + attachmentName = path.get(2); + if (attachmentName.startsWith("_") && docID.startsWith("_design")) { + // Design-doc attribute like _info or _view + message = message + .replaceFirst("_Attachment", "_DesignDocument"); + docID = docID.substring(8); // strip the "_design/" prefix + attachmentName = pathLen > 3 ? path.get(3) : null; + } else { + if (pathLen > 3) { + List subList = path.subList(2, pathLen); + StringBuilder sb = new StringBuilder(); + Iterator iter = subList.iterator(); + while (iter.hasNext()) { + sb.append(iter.next()); + if (iter.hasNext()) { + // sb.append("%2F"); + sb.append("/"); + } + } + attachmentName = sb.toString(); + } + } + } + + // Log.d(TAG, "path: " + path + " message: " + message + " docID: " + + // docID + " attachmentName: " + attachmentName); + + // Send myself a message based on the components: + TDStatus status = new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + try { + Method m = this.getClass().getMethod(message, TDDatabase.class, + String.class, String.class); + status = (TDStatus) m.invoke(this, db, docID, attachmentName); + } catch (NoSuchMethodException msme) { + try { + Method m = this.getClass().getMethod("do_UNKNOWN", + TDDatabase.class, String.class, String.class); + status = (TDStatus) m.invoke(this, db, docID, attachmentName); + } catch (Exception e) { + // default status is internal server error + } + } catch (Exception e) { + // default status is internal server error + Log.e(TDDatabase.TAG, "Exception in TDRouter", e); + } + + // Configure response headers: + if (status.isSuccessful() && connection.getResponseBody() == null + && connection.getHeaderField("Content-Type") == null) { + connection.setResponseBody(new TDBody("{\"ok\":true}".getBytes())); + } + + if (connection.getResponseBody() != null + && connection.getResponseBody().isValidJSON()) { + connection.getResHeader().add("Content-Type", "application/json"); + } + + // Check for a mismatch between the Accept request header and the + // response type: + String accept = connection.getRequestProperty("Accept"); + if (accept != null && !"*/*".equals(accept)) { + String responseType = connection.getBaseContentType(); + if (responseType != null && accept.indexOf(responseType) < 0) { + Log.e(TDDatabase.TAG, String.format( + "Error 406: Can't satisfy request Accept: %s", accept)); + status = new TDStatus(TDStatus.NOT_ACCEPTABLE); + } + } + + connection.getResHeader().add("Server", + String.format("TouchDB %s", getVersionString())); + + // If response is ready (nonzero status), tell my client about it: + if (status.getCode() != 0) { + connection.setResponseCode(status.getCode()); + + if (connection.getResponseBody() != null) { + ByteArrayInputStream bais = new ByteArrayInputStream(connection + .getResponseBody().getJson()); + connection.setResponseInputStream(bais); + } else { + + try { + connection.getResponseOutputStream().close(); + } catch (IOException e) { + Log.e(TDDatabase.TAG, "Error closing empty output stream"); + } + } + sendResponse(); + } + } + + public void stop() { + callbackBlock = null; + if (db != null) { + db.deleteObserver(this); + } + } + + public TDStatus do_UNKNOWN(TDDatabase db, String docID, + String attachmentName) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + /*************************************************************************************************/ + /*** TDRouter+Handlers ***/ + /*************************************************************************************************/ + + public void setResponseLocation(URL url) { + String location = url.toExternalForm(); + String query = url.getQuery(); + if (query != null) { + int startOfQuery = location.indexOf(query); + if (startOfQuery > 0) { + location = location.substring(0, startOfQuery); + } + } + connection.getResHeader().add("Location", location); + } + + /** SERVER REQUESTS: **/ + + public TDStatus do_GETRoot(TDDatabase _db, String _docID, + String _attachmentName) { + Map info = new HashMap(); + info.put("TouchDB", "Welcome"); + info.put("couchdb", "Welcome"); // for compatibility + info.put("version", getVersionString()); + connection.setResponseBody(new TDBody(info)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_GET_all_dbs(TDDatabase _db, String _docID, + String _attachmentName) { + List dbs = server.allDatabaseNames(); + connection.setResponseBody(new TDBody(dbs)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_GET_session(TDDatabase _db, String _docID, + String _attachmentName) { + // Send back an "Admin Party"-like response + Map session = new HashMap(); + Map userCtx = new HashMap(); + String[] roles = { "_admin" }; + session.put("ok", true); + userCtx.put("name", null); + userCtx.put("roles", roles); + session.put("userCtx", userCtx); + connection.setResponseBody(new TDBody(session)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_POST_replicate(TDDatabase _db, String _docID, + String _attachmentName) { + // Extract the parameters from the JSON request body: + // http://wiki.apache.org/couchdb/Replication + Map body = getBodyAsDictionary(); + if (body == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + String source = (String) body.get("source"); + String target = (String) body.get("target"); + Boolean createTargetBoolean = (Boolean) body.get("create_target"); + boolean createTarget = (createTargetBoolean != null && createTargetBoolean + .booleanValue()); + Boolean continuousBoolean = (Boolean) body.get("continuous"); + boolean continuous = (continuousBoolean != null && continuousBoolean + .booleanValue()); + Boolean cancelBoolean = (Boolean) body.get("cancel"); + boolean cancel = (cancelBoolean != null && cancelBoolean.booleanValue()); + String access_token = (String) ((Map) body + .get("query_params")).get("access_token"); + + // Map the 'source' and 'target' JSON params to a local database and + // remote URL: + if (source == null || target == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + boolean push = false; + TDDatabase db = server.getExistingDatabaseNamed(source); + String remoteStr = null; + if (db != null) { + remoteStr = target; + push = true; + } else { + remoteStr = source; + if (createTarget && !cancel) { + db = server.getDatabaseNamed(target); + if (!db.open()) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + } else { + db = server.getExistingDatabaseNamed(target); + } + if (db == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + } + + URL remote = null; + try { + remote = new URL(remoteStr); + } catch (MalformedURLException e) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + if (remote == null || !remote.getProtocol().startsWith("http")) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + if (!cancel) { + // Start replication: + TDReplicator repl = db.getReplicator(remote, + server.getDefaultHttpClientFactory(), push, access_token, + continuous, server.getWorkExecutor()); + if (repl == null) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + String filterName = (String) body.get("filter"); + if (filterName != null) { + repl.setFilterName(filterName); + Map filterParams = (Map) body + .get("query_params"); + if (filterParams != null) { + repl.setFilterParams(filterParams); + } + } + + if (push) { + ((TDPusher) repl).setCreateTarget(createTarget); + } + repl.start(); + Map result = new HashMap(); + result.put("session_id", repl.getSessionID()); + connection.setResponseBody(new TDBody(result)); + } else { + // Cancel replication: + TDReplicator repl = db.getActiveReplicator(remote, push); + if (repl == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + repl.stop(); + } + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_GET_uuids(TDDatabase _db, String _docID, + String _attachmentName) { + int count = Math.min(1000, getIntQuery("count", 1)); + List uuids = new ArrayList(count); + for (int i = 0; i < count; i++) { + uuids.add(TDDatabase.generateDocumentId()); + } + Map result = new HashMap(); + result.put("uuids", uuids); + connection.setResponseBody(new TDBody(result)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_GET_active_tasks(TDDatabase _db, String _docID, + String _attachmentName) { + // http://wiki.apache.org/couchdb/HttpGetActiveTasks + List> activities = new ArrayList>(); + for (TDDatabase db : server.allOpenDatabases()) { + List activeReplicators = db.getActiveReplicators(); + if (activeReplicators != null) { + for (TDReplicator replicator : activeReplicators) { + String source = replicator.getRemote().toExternalForm(); + String target = db.getName(); + if (replicator.isPush()) { + String tmp = source; + source = target; + target = tmp; + } + int processed = replicator.getChangesProcessed(); + int total = replicator.getChangesTotal(); + String status = String.format("Processed %d / %d changes", + processed, total); + int progress = (total > 0) ? Math.round(100 * processed + / (float) total) : 0; + Map activity = new HashMap(); + activity.put("type", "Replication"); + activity.put("task", replicator.getSessionID()); + activity.put("source", source); + activity.put("target", target); + activity.put("status", status); + activity.put("progress", progress); + activities.add(activity); + } + } + } + connection.setResponseBody(new TDBody(activities)); + return new TDStatus(TDStatus.OK); + } + + /** DATABASE REQUESTS: **/ + + public TDStatus do_GET_Database(TDDatabase _db, String _docID, + String _attachmentName) { + // http://wiki.apache.org/couchdb/HTTP_database_API#Database_Information + TDStatus status = openDB(); + if (!status.isSuccessful()) { + return status; + } + int num_docs = db.getDocumentCount(); + long update_seq = db.getLastSequence(); + Map result = new HashMap(); + result.put("db_name", db.getName()); + result.put("db_uuid", db.publicUUID()); + result.put("doc_count", num_docs); + result.put("update_seq", update_seq); + result.put("disk_size", db.totalDataSize()); + connection.setResponseBody(new TDBody(result)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_PUT_Database(TDDatabase _db, String _docID, + String _attachmentName) { + if (db.exists()) { + return new TDStatus(TDStatus.PRECONDITION_FAILED); + } + if (!db.open()) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + setResponseLocation(connection.getURL()); + return new TDStatus(TDStatus.CREATED); + } + + public TDStatus do_DELETE_Database(TDDatabase _db, String _docID, + String _attachmentName) { + if (getQuery("rev") != null) { + return new TDStatus(TDStatus.BAD_REQUEST); // CouchDB checks for + // this; probably meant + // to be a document + // deletion + } + return server.deleteDatabaseNamed(db.getName()) ? new TDStatus( + TDStatus.OK) : new TDStatus(TDStatus.NOT_FOUND); + } + + public TDStatus do_POST_Database(TDDatabase _db, String _docID, + String _attachmentName) { + TDStatus status = openDB(); + if (!status.isSuccessful()) { + return status; + } + return update(db, null, getBodyAsDictionary(), false); + } + + public TDStatus do_GET_Document_all_docs(TDDatabase _db, String _docID, + String _attachmentName) { + TDQueryOptions options = new TDQueryOptions(); + if (!getQueryOptions(options)) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + Map result = db.getAllDocs(options); + if (result == null) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + connection.setResponseBody(new TDBody(result)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_POST_Document_all_docs(TDDatabase _db, String _docID, + String _attachmentName) { + TDQueryOptions options = new TDQueryOptions(); + if (!getQueryOptions(options)) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + Map body = getBodyAsDictionary(); + if (body == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + Map result = null; + if (body.containsKey("keys") && body.get("keys") instanceof ArrayList) { + ArrayList keys = (ArrayList) body.get("keys"); + result = db.getDocsWithIDs(keys, options); + } else { + result = db.getAllDocs(options); + } + + if (result == null) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + connection.setResponseBody(new TDBody(result)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_POST_Document_bulk_docs(TDDatabase _db, String _docID, + String _attachmentName) { + Map bodyDict = getBodyAsDictionary(); + if (bodyDict == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + List> docs = (List>) bodyDict + .get("docs"); + + boolean allObj = false; + if (getQuery("all_or_nothing") == null + || (getQuery("all_or_nothing") != null && (new Boolean( + getQuery("all_or_nothing"))))) { + allObj = true; + } + // allowConflict If false, an error status 409 will be returned if the + // insertion would create a conflict, i.e. if the previous revision + // already has a child. + boolean allOrNothing = (allObj && allObj != false); + boolean noNewEdits = true; + if (getQuery("new_edits") == null + || (getQuery("new_edits") != null && (new Boolean( + getQuery("new_edits"))))) { + noNewEdits = false; + } + boolean ok = false; + db.beginTransaction(); + List> results = new ArrayList>(); + try { + for (Map doc : docs) { + String docID = (String) doc.get("_id"); + TDRevision rev = null; + TDStatus status = new TDStatus(TDStatus.BAD_REQUEST); + TDBody docBody = new TDBody(doc); + if (noNewEdits) { + rev = new TDRevision(docBody); + if (rev.getRevId() == null || rev.getDocId() == null + || !rev.getDocId().equals(docID)) { + status = new TDStatus(TDStatus.BAD_REQUEST); + } else { + List history = TDDatabase + .parseCouchDBRevisionHistory(doc); + status = db.forceInsert(rev, history, null); + } + } else { + TDStatus outStatus = new TDStatus(); + rev = update(db, docID, docBody, false, allOrNothing, + outStatus); + status.setCode(outStatus.getCode()); + } + Map result = null; + if (status.isSuccessful()) { + result = new HashMap(); + result.put("ok", true); + result.put("id", docID); + if (rev != null) { + result.put("rev", rev.getRevId()); + } + } else if (allOrNothing) { + return status; // all_or_nothing backs out if there's any + // error + } else if (status.getCode() == TDStatus.FORBIDDEN) { + result = new HashMap(); + result.put("error", "validation failed"); + result.put("id", docID); + } else if (status.getCode() == TDStatus.CONFLICT) { + result = new HashMap(); + result.put("error", "conflict"); + result.put("id", docID); + } else { + return status; // abort the whole thing if something goes + // badly wrong + } + if (result != null) { + results.add(result); + } + } + Log.w(TDDatabase.TAG, String.format( + "%s finished inserting %d revisions in bulk", this, + docs.size())); + ok = true; + } catch (Exception e) { + Log.w(TDDatabase.TAG, String.format( + "%s: Exception inserting revisions in bulk", this), e); + } finally { + db.endTransaction(ok); + } + Log.d(TDDatabase.TAG, "results: " + results.toString()); + connection.setResponseBody(new TDBody(results)); + return new TDStatus(TDStatus.CREATED); + } + + public TDStatus do_POST_Document_revs_diff(TDDatabase _db, String _docID, + String _attachmentName) { + // http://wiki.apache.org/couchdb/HttpPostRevsDiff + // Collect all of the input doc/revision IDs as TDRevisions: + TDRevisionList revs = new TDRevisionList(); + Map body = getBodyAsDictionary(); + if (body == null) { + return new TDStatus(TDStatus.BAD_JSON); + } + for (String docID : body.keySet()) { + List revIDs = (List) body.get(docID); + for (String revID : revIDs) { + TDRevision rev = new TDRevision(docID, revID, false); + revs.add(rev); + } + } + + // Look them up, removing the existing ones from revs: + if (db.findMissingRevisions(revs) != null) { + return new TDStatus(TDStatus.DB_ERROR); + } + + // Return the missing revs in a somewhat different format: + Map diffs = new HashMap(); + for (TDRevision rev : revs) { + String docID = rev.getDocId(); + + List missingRevs = null; + Map idObj = (Map) diffs.get(docID); + if (idObj != null) { + missingRevs = (List) idObj.get("missing"); + } else { + idObj = new HashMap(); + } + + if (missingRevs == null) { + missingRevs = new ArrayList(); + idObj.put("missing", missingRevs); + diffs.put(docID, idObj); + } + missingRevs.add(rev.getRevId()); + } + + // FIXME add support for possible_ancestors + + connection.setResponseBody(new TDBody(diffs)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_POST_Document_compact(TDDatabase _db, String _docID, + String _attachmentName) { + TDStatus status = _db.compact(); + if (status.getCode() < 300) { + TDStatus outStatus = new TDStatus(); + outStatus.setCode(202); // CouchDB returns 202 'cause it's an async + // operation + return outStatus; + } else { + return status; + } + } + + public TDStatus do_POST_Document_ensure_full_commit(TDDatabase _db, + String _docID, String _attachmentName) { + return new TDStatus(TDStatus.OK); + } + + /** CHANGES: **/ + + public Map changesDictForRevision(TDRevision rev) { + Map changesDict = new HashMap(); + changesDict.put("rev", rev.getRevId()); + + List> changes = new ArrayList>(); + changes.add(changesDict); + + Map result = new HashMap(); + result.put("seq", rev.getSequence()); + result.put("id", rev.getDocId()); + result.put("changes", changes); + if (rev.isDeleted()) { + result.put("deleted", true); + } + if (changesIncludesDocs) { + result.put("doc", rev.getProperties()); + } + return result; + } + + public Map responseBodyForChanges(List changes, + long since) { + List> results = new ArrayList>(); + for (TDRevision rev : changes) { + Map changeDict = changesDictForRevision(rev); + results.add(changeDict); + } + if (changes.size() > 0) { + since = changes.get(changes.size() - 1).getSequence(); + } + Map result = new HashMap(); + result.put("results", results); + result.put("last_seq", since); + return result; + } + + public Map responseBodyForChangesWithConflicts( + List changes, long since) { + // Assumes the changes are grouped by docID so that conflicts will be + // adjacent. + List> entries = new ArrayList>(); + String lastDocID = null; + Map lastEntry = null; + for (TDRevision rev : changes) { + String docID = rev.getDocId(); + if (docID.equals(lastDocID)) { + Map changesDict = new HashMap(); + changesDict.put("rev", rev.getRevId()); + List> inchanges = (List>) lastEntry + .get("changes"); + inchanges.add(changesDict); + } else { + lastEntry = changesDictForRevision(rev); + entries.add(lastEntry); + lastDocID = docID; + } + } + // After collecting revisions, sort by sequence: + Collections.sort(entries, new Comparator>() { + public int compare(Map e1, Map e2) { + return TDMisc.TDSequenceCompare((Long) e1.get("seq"), + (Long) e2.get("seq")); + } + }); + + Long lastSeq = (Long) entries.get(entries.size() - 1).get("seq"); + if (lastSeq == null) { + lastSeq = since; + } + + Map result = new HashMap(); + result.put("results", entries); + result.put("last_seq", lastSeq); + return result; + } + + public void sendContinuousChange(TDRevision rev) { + Map changeDict = changesDictForRevision(rev); + try { + String jsonString = TDServer.getObjectMapper().writeValueAsString( + changeDict); + if (callbackBlock != null) { + byte[] json = (jsonString + "\n").getBytes(); + OutputStream os = connection.getResponseOutputStream(); + try { + os.write(json); + os.flush(); + } catch (Exception e) { + Log.e(TDDatabase.TAG, + "IOException writing to internal streams", e); + } + } + } catch (Exception e) { + Log.w("Unable to serialize change to JSON", e); + } + } + + @Override + public void update(Observable observable, Object changeObject) { + if (observable == db) { + // make sure we're listening to the right events + Map changeNotification = (Map) changeObject; + + TDRevision rev = (TDRevision) changeNotification.get("rev"); + + if (changesFilter != null && !changesFilter.filter(rev)) { + return; + } + + if (longpoll) { + Log.w(TDDatabase.TAG, "TDRouter: Sending longpoll response"); + sendResponse(); + List revs = new ArrayList(); + revs.add(rev); + Map body = responseBodyForChanges(revs, 0); + if (callbackBlock != null) { + byte[] data = null; + try { + data = TDServer.getObjectMapper().writeValueAsBytes( + body); + } catch (Exception e) { + Log.w(TDDatabase.TAG, "Error serializing JSON", e); + } + OutputStream os = connection.getResponseOutputStream(); + try { + os.write(data); + os.close(); + } catch (IOException e) { + Log.e(TDDatabase.TAG, + "IOException writing to internal streams", e); + } + } + } else { + Log.w(TDDatabase.TAG, + "TDRouter: Sending continous change chunk"); + sendContinuousChange(rev); + } + + } + + } + + public TDStatus do_GET_Document_changes(TDDatabase _db, String docID, + String _attachmentName) { + // http://wiki.apache.org/couchdb/HTTP_database_API#Changes + TDChangesOptions options = new TDChangesOptions(); + changesIncludesDocs = getBooleanQuery("include_docs"); + options.setIncludeDocs(changesIncludesDocs); + String style = getQuery("style"); + if (style != null && style.equals("all_docs")) { + options.setIncludeConflicts(true); + } + options.setContentOptions(getContentOptions()); + options.setSortBySequence(!options.isIncludeConflicts()); + options.setLimit(getIntQuery("limit", options.getLimit())); + + int since = getIntQuery("since", 0); + + String filterName = getQuery("filter"); + if (filterName != null) { + changesFilter = db.getFilterNamed(filterName); + if (changesFilter == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + } + + TDRevisionList changes = db.changesSince(since, options, changesFilter); + + if (changes == null) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + + String feed = getQuery("feed"); + longpoll = "longpoll".equals(feed); + boolean continuous = !longpoll && "continuous".equals(feed); + + if (continuous || (longpoll && changes.size() == 0)) { + connection.setChunked(true); + connection.setResponseCode(TDStatus.OK); + sendResponse(); + if (continuous) { + for (TDRevision rev : changes) { + sendContinuousChange(rev); + } + } + db.addObserver(this); + // Don't close connection; more data to come + return new TDStatus(0); + } else { + if (options.isIncludeConflicts()) { + connection.setResponseBody(new TDBody( + responseBodyForChangesWithConflicts(changes, since))); + } else { + connection.setResponseBody(new TDBody(responseBodyForChanges( + changes, since))); + } + return new TDStatus(TDStatus.OK); + } + } + + /** DOCUMENT REQUESTS: **/ + + public String getRevIDFromIfMatchHeader() { + String ifMatch = connection.getRequestProperty("If-Match"); + if (ifMatch == null) { + return null; + } + // Value of If-Match is an ETag, so have to trim the quotes around it: + if (ifMatch.length() > 2 && ifMatch.startsWith("\"") + && ifMatch.endsWith("\"")) { + return ifMatch.substring(1, ifMatch.length() - 2); + } else { + return null; + } + } + + public String setResponseEtag(TDRevision rev) { + String eTag = String.format("\"%s\"", rev.getRevId()); + connection.getResHeader().add("Etag", eTag); + return eTag; + } + + public TDStatus do_GET_Document(TDDatabase _db, String docID, + String _attachmentName) { + // http://wiki.apache.org/couchdb/HTTP_Document_API#GET + boolean isLocalDoc = docID.startsWith("_local"); + EnumSet options = getContentOptions(); + String openRevsParam = getQuery("open_revs"); + if (openRevsParam == null || isLocalDoc) { + // Regular GET: + String revID = getQuery("rev"); // often null + TDRevision rev = null; + if (isLocalDoc) { + rev = db.getLocalDocument(docID, revID); + } else { + rev = db.getDocumentWithIDAndRev(docID, revID, options); + // Handle ?atts_since query by stubbing out older attachments: + // ?atts_since parameter - value is a (URL-encoded) JSON array + // of one or more revision IDs. + // The response will include the content of only those + // attachments that changed since the given revision(s). + // (You can ask for this either in the default JSON or as + // multipart/related, as previously described.) + List attsSince = (List) getJSONQuery("atts_since"); + if (attsSince != null) { + String ancestorId = db.findCommonAncestorOf(rev, attsSince); + if (ancestorId != null) { + int generation = TDRevision + .generationFromRevID(ancestorId); + db.stubOutAttachmentsIn(rev, generation + 1); + } + } + } + if (rev == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + if (cacheWithEtag(rev.getRevId())) { + return new TDStatus(TDStatus.NOT_MODIFIED); // set ETag and + // check conditional + // GET + } + + connection.setResponseBody(rev.getBody()); + } else { + List> result = null; + if (openRevsParam.equals("all")) { + // Get all conflicting revisions: + TDRevisionList allRevs = db.getAllRevisionsOfDocumentID(docID, + true); + result = new ArrayList>(allRevs.size()); + for (TDRevision rev : allRevs) { + TDStatus status = db.loadRevisionBody(rev, options); + if (status.isSuccessful()) { + Map dict = new HashMap(); + dict.put("ok", rev.getProperties()); + result.add(dict); + } else if (status.getCode() != TDStatus.INTERNAL_SERVER_ERROR) { + Map dict = new HashMap(); + dict.put("missing", rev.getRevId()); + result.add(dict); + } else { + return status; // internal error getting revision + } + } + } else { + // ?open_revs=[...] returns an array of revisions of the + // document: + List openRevs = (List) getJSONQuery("open_revs"); + if (openRevs == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + result = new ArrayList>(openRevs.size()); + for (String revID : openRevs) { + TDRevision rev = db.getDocumentWithIDAndRev(docID, revID, + options); + if (rev != null) { + Map dict = new HashMap(); + dict.put("ok", rev.getProperties()); + result.add(dict); + } else { + Map dict = new HashMap(); + dict.put("missing", revID); + result.add(dict); + } + } + } + String acceptMultipart = getMultipartRequestType(); + if (acceptMultipart != null) { + // FIXME figure out support for multipart + throw new UnsupportedOperationException(); + } else { + connection.setResponseBody(new TDBody(result)); + } + } + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_GET_Attachment(TDDatabase _db, String docID, + String _attachmentName) { + // http://wiki.apache.org/couchdb/HTTP_Document_API#GET + EnumSet options = getContentOptions(); + options.add(TDContentOptions.TDNoBody); + String revID = getQuery("rev"); // often null + TDRevision rev = db.getDocumentWithIDAndRev(docID, revID, options); + if (rev == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + if (cacheWithEtag(rev.getRevId())) { + return new TDStatus(TDStatus.NOT_MODIFIED); // set ETag and check + // conditional GET + } + + String type = null; + TDStatus status = new TDStatus(); + String acceptEncoding = connection + .getRequestProperty("Accept-Encoding"); + TDAttachment contents = db.getAttachmentForSequence(rev.getSequence(), + _attachmentName, status); + + if (contents == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + type = contents.getContentType(); + if (type != null) { + connection.getResHeader().add("Content-Type", type); + } + if (acceptEncoding != null && acceptEncoding.equals("gzip")) { + connection.getResHeader().add("Content-Encoding", acceptEncoding); + } + + connection.setResponseInputStream(contents.getContentStream()); + return new TDStatus(TDStatus.OK); + } + + /** + * NOTE this departs from the iOS version, returning revision, passing + * status back by reference + */ + public TDRevision update(TDDatabase _db, String docID, TDBody body, + boolean deleting, boolean allowConflict, TDStatus outStatus) { + boolean isLocalDoc = docID != null && docID.startsWith(("_local")); + String prevRevID = null; + + if (!deleting) { + Boolean deletingBoolean = (Boolean) body + .getPropertyForKey("deleted"); + deleting = (deletingBoolean != null && deletingBoolean + .booleanValue()); + if (docID == null) { + if (isLocalDoc) { + outStatus.setCode(TDStatus.METHOD_NOT_ALLOWED); + return null; + } + // POST's doc ID may come from the _id field of the JSON body, + // else generate a random one. + docID = (String) body.getPropertyForKey("_id"); + if (docID == null) { + if (deleting) { + outStatus.setCode(TDStatus.BAD_REQUEST); + return null; + } + docID = TDDatabase.generateDocumentId(); + } + } + // PUT's revision ID comes from the JSON body. + prevRevID = (String) body.getPropertyForKey("_rev"); + } else { + // DELETE's revision ID comes from the ?rev= query param + prevRevID = getQuery("rev"); + } + + // A backup source of revision ID is an If-Match header: + if (prevRevID == null) { + prevRevID = getRevIDFromIfMatchHeader(); + } + + TDRevision rev = new TDRevision(docID, null, deleting); + rev.setBody(body); + + TDRevision result = null; + TDStatus tmpStatus = new TDStatus(); + if (isLocalDoc) { + result = _db.putLocalRevision(rev, prevRevID, tmpStatus); + } else { + result = _db.putRevision(rev, prevRevID, allowConflict, tmpStatus); + } + outStatus.setCode(tmpStatus.getCode()); + return result; + } + + public TDStatus update(TDDatabase _db, String docID, + Map bodyDict, boolean deleting) { + TDBody body = new TDBody(bodyDict); + TDStatus status = new TDStatus(); + TDRevision rev = update(_db, docID, body, deleting, false, status); + if (status.isSuccessful()) { + cacheWithEtag(rev.getRevId()); // set ETag + if (!deleting) { + URL url = connection.getURL(); + String urlString = url.toExternalForm(); + if (docID != null) { + urlString += "/" + rev.getDocId(); + try { + url = new URL(urlString); + } catch (MalformedURLException e) { + Log.w("Malformed URL", e); + } + } + setResponseLocation(url); + } + Map result = new HashMap(); + result.put("ok", true); + result.put("id", rev.getDocId()); + result.put("rev", rev.getRevId()); + connection.setResponseBody(new TDBody(result)); + } + return status; + } + + public TDStatus do_PUT_Document(TDDatabase _db, String docID, + String _attachmentName) { + Map bodyDict = getBodyAsDictionary(); + if (bodyDict == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + + if (getQuery("new_edits") == null + || (getQuery("new_edits") != null && (new Boolean( + getQuery("new_edits"))))) { + // Regular PUT + return update(_db, docID, bodyDict, false); + } else { + // PUT with new_edits=false -- forcible insertion of existing + // revision: + TDBody body = new TDBody(bodyDict); + TDRevision rev = new TDRevision(body); + if (rev.getRevId() == null || rev.getDocId() == null + || !rev.getDocId().equals(docID)) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + List history = TDDatabase.parseCouchDBRevisionHistory(body + .getProperties()); + return db.forceInsert(rev, history, null); + } + } + + public TDStatus do_DELETE_Document(TDDatabase _db, String docID, + String _attachmentName) { + return update(_db, docID, null, true); + } + + public TDStatus updateAttachment(String attachment, String docID, + InputStream contentStream) { + TDStatus status = new TDStatus(); + String revID = getQuery("rev"); + if (revID == null) { + revID = getRevIDFromIfMatchHeader(); + } + TDRevision rev = db.updateAttachment(attachment, contentStream, + connection.getRequestProperty("content-type"), docID, revID, + status); + if (status.isSuccessful()) { + Map resultDict = new HashMap(); + resultDict.put("ok", true); + resultDict.put("id", rev.getDocId()); + resultDict.put("rev", rev.getRevId()); + connection.setResponseBody(new TDBody(resultDict)); + cacheWithEtag(rev.getRevId()); + if (contentStream != null) { + setResponseLocation(connection.getURL()); + } + } + return status; + } + + public TDStatus do_PUT_Attachment(TDDatabase _db, String docID, + String _attachmentName) { + return updateAttachment(_attachmentName, docID, + connection.getRequestInputStream()); + } + + public TDStatus do_DELETE_Attachment(TDDatabase _db, String docID, + String _attachmentName) { + return updateAttachment(_attachmentName, docID, null); + } + + /** VIEW QUERIES: **/ + + public TDView compileView(String viewName, Map viewProps) { + String language = (String) viewProps.get("language"); + if (language == null) { + language = "javascript"; + } + String mapSource = (String) viewProps.get("map"); + if (mapSource == null) { + return null; + } + TDViewMapBlock mapBlock = TDView.getCompiler().compileMapFunction( + mapSource, language); + if (mapBlock == null) { + Log.w(TDDatabase.TAG, String + .format("View %s has unknown map function: %s", viewName, + mapSource)); + return null; + } + String reduceSource = (String) viewProps.get("reduce"); + TDViewReduceBlock reduceBlock = null; + if (reduceSource != null) { + reduceBlock = TDView.getCompiler().compileReduceFunction( + reduceSource, language); + if (reduceBlock == null) { + Log.w(TDDatabase.TAG, String.format( + "View %s has unknown reduce function: %s", viewName, + reduceBlock)); + return null; + } + } + + TDView view = db.getViewNamed(viewName); + view.setMapReduceBlocks(mapBlock, reduceBlock, "1"); + String collation = (String) viewProps.get("collation"); + if ("raw".equals(collation)) { + view.setCollation(TDViewCollation.TDViewCollationRaw); + } + return view; + } + + public TDStatus queryDesignDoc(String designDoc, String viewName, + List keys) { + String tdViewName = String.format("%s/%s", designDoc, viewName); + TDView view = db.getExistingViewNamed(tdViewName); + if (view == null || view.getMapBlock() == null) { + // No TouchDB view is defined, or it hasn't had a map block + // assigned; + // see if there's a CouchDB view definition we can compile: + TDRevision rev = db.getDocumentWithIDAndRev( + String.format("_design/%s", designDoc), null, + EnumSet.noneOf(TDContentOptions.class)); + if (rev == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + Map views = (Map) rev + .getProperties().get("views"); + Map viewProps = (Map) views + .get(viewName); + if (viewProps == null) { + return new TDStatus(TDStatus.NOT_FOUND); + } + // If there is a CouchDB view, see if it can be compiled from + // source: + view = compileView(tdViewName, viewProps); + if (view == null) { + return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); + } + } + + TDQueryOptions options = new TDQueryOptions(); + + // if the view contains a reduce block, it should default to reduce=true + if (view.getReduceBlock() != null) { + options.setReduce(true); + } + + if (!getQueryOptions(options)) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + if (keys != null) { + options.setKeys(keys); + } + + TDStatus status = view.updateIndex(); + if (!status.isSuccessful()) { + return status; + } + + long lastSequenceIndexed = view.getLastSequenceIndexed(); + + // Check for conditional GET and set response Etag header: + if (keys == null) { + long eTag = options.isIncludeDocs() ? db.getLastSequence() + : lastSequenceIndexed; + if (cacheWithEtag(String.format("%d", eTag))) { + return new TDStatus(TDStatus.NOT_MODIFIED); + } + } + + List> rows = view.queryWithOptions(options, status); + if (rows == null) { + return status; + } + + Map responseBody = new HashMap(); + responseBody.put("rows", rows); + responseBody.put("total_rows", rows.size()); + responseBody.put("offset", options.getSkip()); + if (options.isUpdateSeq()) { + responseBody.put("update_seq", lastSequenceIndexed); + } + connection.setResponseBody(new TDBody(responseBody)); + return new TDStatus(TDStatus.OK); + } + + public TDStatus do_GET_DesignDocument(TDDatabase _db, String designDocID, + String viewName) { + return queryDesignDoc(designDocID, viewName, null); + } + + public TDStatus do_POST_DesignDocument(TDDatabase _db, String designDocID, + String viewName) { + Map bodyDict = getBodyAsDictionary(); + if (bodyDict == null) { + return new TDStatus(TDStatus.BAD_REQUEST); + } + List keys = (List) bodyDict.get("keys"); + return queryDesignDoc(designDocID, viewName, keys); + } + + @Override + public String toString() { + String url = "Unknown"; + if (connection != null && connection.getURL() != null) { + url = connection.getURL().toExternalForm(); + } + return String.format("TDRouter [%s]", url); + } } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java b/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java index 238bdbb..91716fa 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java @@ -33,7 +33,6 @@ import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; -import android.os.Handler; import android.util.Log; import com.couchbase.touchdb.TDDatabase; From 460a1cd5f0ed32f203d747ec2c6ccd5008de9400 Mon Sep 17 00:00:00 2001 From: Sameer Segal Date: Thu, 1 May 2014 12:20:07 +0530 Subject: [PATCH 11/11] just saving an old commit --- TouchDB-Android-Ektorp/project.properties | 2 +- TouchDB-Android/.classpath | 4 +- TouchDB-Android/project.properties | 2 +- .../src/com/couchbase/touchdb/TDDatabase.java | 15 +- .../touchdb/replicator/TDPuller.java | 15 +- .../touchdb/replicator/TDPusher.java | 65 ++-- .../touchdb/replicator/TDReplicator.java | 24 +- .../couchbase/touchdb/router/TDRouter.java | 24 +- .../touchdb/support/TDRemoteRequest.java | 303 +++++++++--------- 9 files changed, 250 insertions(+), 204 deletions(-) diff --git a/TouchDB-Android-Ektorp/project.properties b/TouchDB-Android-Ektorp/project.properties index 63f16dd..eda01e1 100644 --- a/TouchDB-Android-Ektorp/project.properties +++ b/TouchDB-Android-Ektorp/project.properties @@ -8,6 +8,6 @@ # project structure. # Project target. -target=android-17 +target=Google Inc.:Google APIs:15 android.library=true android.library.reference.1=../TouchDB-Android diff --git a/TouchDB-Android/.classpath b/TouchDB-Android/.classpath index 785d3a4..374d658 100644 --- a/TouchDB-Android/.classpath +++ b/TouchDB-Android/.classpath @@ -5,8 +5,8 @@ - - + + diff --git a/TouchDB-Android/project.properties b/TouchDB-Android/project.properties index e61077f..4c10f5c 100644 --- a/TouchDB-Android/project.properties +++ b/TouchDB-Android/project.properties @@ -8,5 +8,5 @@ # project structure. # Project target. -target=android-17 +target=Google Inc.:Google APIs:15 android.library=true diff --git a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java index 16bde41..aefa66b 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/TDDatabase.java @@ -2407,24 +2407,25 @@ public TDReplicator getActiveReplicator(URL remote, boolean push) { } public TDReplicator getReplicator(URL remote, boolean push, - String access_token, boolean continuous, + String access_token, Map headers, boolean continuous, ScheduledExecutorService workExecutor) { TDReplicator replicator = getReplicator(remote, null, push, - access_token, continuous, workExecutor); + access_token, headers, continuous, workExecutor); return replicator; } public TDReplicator getReplicator(URL remote, HttpClientFactory httpClientFactory, boolean push, - String access_token, boolean continuous, - ScheduledExecutorService workExecutor) { + String access_token, Map headers, + boolean continuous, ScheduledExecutorService workExecutor) { TDReplicator result = getActiveReplicator(remote, push); if (result != null) { return result; } - result = push ? new TDPusher(this, remote, access_token, continuous, - httpClientFactory, workExecutor) : new TDPuller(this, remote, - access_token, continuous, httpClientFactory, workExecutor); + result = push ? new TDPusher(this, remote, access_token, headers, + continuous, httpClientFactory, workExecutor) : new TDPuller( + this, remote, access_token, headers, continuous, + httpClientFactory, workExecutor); if (activeReplicators == null) { activeReplicators = new ArrayList(); diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java index 4a5c0ae..d7bab9b 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPuller.java @@ -43,14 +43,17 @@ public class TDPuller extends TDReplicator implements TDChangeTrackerClient { protected int httpConnectionCount; public TDPuller(TDDatabase db, URL remote, String access_token, - boolean continuous, ScheduledExecutorService workExecutor) { - this(db, remote, access_token, continuous, null, workExecutor); + Map headers, boolean continuous, + ScheduledExecutorService workExecutor) { + this(db, remote, access_token, headers, continuous, null, workExecutor); } public TDPuller(TDDatabase db, URL remote, String access_token, - boolean continuous, HttpClientFactory clientFactory, + Map headers, boolean continuous, + HttpClientFactory clientFactory, ScheduledExecutorService workExecutor) { - super(db, remote, access_token, continuous, clientFactory, workExecutor); + super(db, remote, access_token, headers, continuous, clientFactory, + workExecutor); } @Override @@ -237,7 +240,7 @@ public void processInbox(TDRevisionList inbox) { // long seq = pendingSequences.addValue(lastInboxSequence); // pendingSequences.removeSequence(seq); // setLastSequence(pendingSequences.getCheckpointedValue()); - + refiller_scheduled.set(false); return; } @@ -340,7 +343,7 @@ public void pullRemoteRevision(final TDRevision rev) { // create a final version of this variable for the log statement inside // FIXME find a way to avoid this final String pathInside = path.toString(); - sendAsyncRequest("GET", pathInside, null, + sendAsyncRequest("GET", pathInside, headers, null, new TDRemoteRequestCompletionBlock() { @Override diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java index d78a866..fcc6db1 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDPusher.java @@ -30,14 +30,17 @@ public class TDPusher extends TDReplicator implements Observer { private TDFilterBlock filter; public TDPusher(TDDatabase db, URL remote, String access_token, - boolean continuous, ScheduledExecutorService workExecutor) { - this(db, remote, access_token, continuous, null, workExecutor); + Map headers, boolean continuous, + ScheduledExecutorService workExecutor) { + this(db, remote, access_token, headers, continuous, null, workExecutor); } public TDPusher(TDDatabase db, URL remote, String access_token, - boolean continuous, HttpClientFactory clientFactory, + Map headers, boolean continuous, + HttpClientFactory clientFactory, ScheduledExecutorService workExecutor) { - super(db, remote, access_token, continuous, clientFactory, workExecutor); + super(db, remote, access_token, headers, continuous, clientFactory, + workExecutor); createTarget = false; observing = false; } @@ -61,28 +64,31 @@ public void maybeCreateRemoteDB() { return; } Log.v(TDDatabase.TAG, "Remote db might not exist; creating it..."); - sendAsyncRequest("PUT", "", null, new TDRemoteRequestCompletionBlock() { - - @Override - public void onCompletion(Object result, Throwable e) { - if (e != null && e instanceof HttpResponseException - && ((HttpResponseException) e).getStatusCode() != 412) { - Log.e(TDDatabase.TAG, "Failed to create remote db", e); - error = e; - stop(); - } else { - Log.v(TDDatabase.TAG, "Created remote db"); - createTarget = false; - beginReplicating(); - } - } + sendAsyncRequest("PUT", "", this.headers, null, + new TDRemoteRequestCompletionBlock() { - }); + @Override + public void onCompletion(Object result, Throwable e) { + if (e != null + && e instanceof HttpResponseException + && ((HttpResponseException) e).getStatusCode() != 412) { + Log.e(TDDatabase.TAG, "Failed to create remote db", + e); + error = e; + stop(); + } else { + Log.v(TDDatabase.TAG, "Created remote db"); + createTarget = false; + beginReplicating(); + } + } + + }); } @Override public void beginReplicating() { - + // If we're still waiting to create the remote db, do nothing now. (This // method will be // re-invoked after that request finishes; see maybeCreateRemoteDB() @@ -125,7 +131,7 @@ public void beginReplicating() { asyncTaskStarted(); // prevents stopped() from being called when // other tasks finish } - + super.beginReplicating(); } @@ -150,7 +156,7 @@ public void update(Observable observable, Object data) { Map change = (Map) data; // Skip revisions that originally came from the database I'm syncing // to: - String source = (String) change.get("source"); + String source = (String) change.get("source"); if (source != null && source.equals(remote.toExternalForm())) { return; } @@ -169,7 +175,7 @@ public void update(Observable observable, Object data) { } @Override - public void processInbox(final TDRevisionList inbox) { + public void processInbox(final TDRevisionList inbox) { if (inbox.size() == 0) { scheduleRefiller(); return; @@ -196,7 +202,7 @@ public void processInbox(final TDRevisionList inbox) { // Call _revs_diff on the target db: asyncTaskStarted(); sendAsyncRequest("POST", "/_revs_diff?access_token=" + access_token, - diffs, new TDRemoteRequestCompletionBlock() { + this.headers, diffs, new TDRemoteRequestCompletionBlock() { @Override public void onCompletion(Object response, Throwable e) { @@ -278,7 +284,7 @@ public void onCompletion(Object response, Throwable e) { setChangesTotal(getChangesTotal() + numDocsToSend); asyncTaskStarted(); sendAsyncRequest("POST", - "/_bulk_docs?access_token=" + access_token, + "/_bulk_docs?access_token=" + access_token, headers, bulkDocsBody, new TDRemoteRequestCompletionBlock() { @@ -302,8 +308,9 @@ public void onCompletion(Object result, setChangesProcessed(getChangesProcessed() + numDocsToSend); asyncTaskFinished(1); - - scheduleRefiller(new Date().getTime()); + + scheduleRefiller(new Date() + .getTime()); } }); @@ -318,7 +325,7 @@ public void onCompletion(Object result, removeLogForRevision(rev); } db.endTransaction(true); - + scheduleRefiller(new Date().getTime()); } asyncTaskFinished(1); diff --git a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java index 2ab2225..998f18a 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/replicator/TDReplicator.java @@ -64,18 +64,23 @@ public abstract class TDReplicator extends Observable { private ExecutorService remoteRequestExecutor; + protected Map headers; + public TDReplicator(TDDatabase db, URL remote, String access_token, - boolean continuous, ScheduledExecutorService workExecutor) { - this(db, remote, access_token, continuous, null, workExecutor); + Map headers, boolean continuous, + ScheduledExecutorService workExecutor) { + this(db, remote, access_token, headers, continuous, null, workExecutor); } public TDReplicator(TDDatabase db, URL remote, String access_token, - boolean continuous, HttpClientFactory clientFacotry, + Map headers, boolean continuous, + HttpClientFactory clientFacotry, ScheduledExecutorService workExecutor) { this.db = db; this.remote = remote; this.access_token = access_token; + this.headers = headers; this.continuous = continuous; this.workExecutor = workExecutor; @@ -264,14 +269,15 @@ public void processInbox(TDRevisionList inbox) { } public void sendAsyncRequest(String method, String relativePath, - Object body, TDRemoteRequestCompletionBlock onCompletion) { + Map headers, Object body, + TDRemoteRequestCompletionBlock onCompletion) { // Log.v(TDDatabase.TAG, String.format("%s: %s .%s", toString(), method, // relativePath)); String urlStr = remote.toExternalForm() + relativePath; try { URL url = new URL(urlStr); TDRemoteRequest request = new TDRemoteRequest(workExecutor, - clientFactory, method, url, body, onCompletion); + clientFactory, method, url, headers, body, onCompletion); remoteRequestExecutor.execute(request); } catch (MalformedURLException e) { Log.e(TDDatabase.TAG, "Malformed URL for async request", e); @@ -341,8 +347,8 @@ public void fetchRemoteCheckpointDoc() { } asyncTaskStarted(); - sendAsyncRequest("GET", "/_local/" + remoteCheckpointDocID(), null, - new TDRemoteRequestCompletionBlock() { + sendAsyncRequest("GET", "/_local/" + remoteCheckpointDocID(), + this.headers, null, new TDRemoteRequestCompletionBlock() { @Override public void onCompletion(Object result, Throwable e) { @@ -411,8 +417,8 @@ public void saveLastSequence() { return; } savingCheckpoint = true; - sendAsyncRequest("PUT", "/_local/" + remoteCheckpointDocID, body, - new TDRemoteRequestCompletionBlock() { + sendAsyncRequest("PUT", "/_local/" + remoteCheckpointDocID, + this.headers, body, new TDRemoteRequestCompletionBlock() { @Override public void onCompletion(Object result, Throwable e) { diff --git a/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java b/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java index 32f82ee..96b6f60 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/router/TDRouter.java @@ -536,8 +536,26 @@ public TDStatus do_POST_replicate(TDDatabase _db, String _docID, .booleanValue()); Boolean cancelBoolean = (Boolean) body.get("cancel"); boolean cancel = (cancelBoolean != null && cancelBoolean.booleanValue()); - String access_token = (String) ((Map) body - .get("query_params")).get("access_token"); + + Map query_params = (Map) body + .get("query_params"); + String access_token = (String) query_params.get("access_token"); + + // All query params starting with header: need to move into headers + Map headers = new HashMap(); + for (String key : query_params.keySet()) { + if (key.startsWith("header:")) { + String value = String.valueOf(query_params.get(key)); + key = key.replaceFirst("header:", ""); + headers.put(key, value); + } + } + + // Remove any headers that are present in query_params. We don't remove + // simultaneously because of concurrent exception + for (String key : headers.keySet()) { + query_params.remove("header:" + key); + } // Map the 'source' and 'target' JSON params to a local database and // remote URL: @@ -579,7 +597,7 @@ public TDStatus do_POST_replicate(TDDatabase _db, String _docID, // Start replication: TDReplicator repl = db.getReplicator(remote, server.getDefaultHttpClientFactory(), push, access_token, - continuous, server.getWorkExecutor()); + headers, continuous, server.getWorkExecutor()); if (repl == null) { return new TDStatus(TDStatus.INTERNAL_SERVER_ERROR); } diff --git a/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java b/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java index 91716fa..5baa6b8 100644 --- a/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java +++ b/TouchDB-Android/src/com/couchbase/touchdb/support/TDRemoteRequest.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import org.apache.http.HttpEntity; @@ -40,151 +41,161 @@ public class TDRemoteRequest implements Runnable { - private ScheduledExecutorService workExecutor; - private final HttpClientFactory clientFactory; - private String method; - private URL url; - private Object body; - private TDRemoteRequestCompletionBlock onCompletion; - - public TDRemoteRequest(ScheduledExecutorService workExecutor, - HttpClientFactory clientFactory, String method, URL url, - Object body, TDRemoteRequestCompletionBlock onCompletion) { - this.clientFactory = clientFactory; - this.method = method; - this.url = url; - this.body = body; - this.onCompletion = onCompletion; - this.workExecutor = workExecutor; - } - - @Override - public void run() { - HttpClient httpClient = clientFactory.getHttpClient(); - ClientConnectionManager manager = httpClient.getConnectionManager(); - - HttpUriRequest request = null; - if (method.equalsIgnoreCase("GET")) { - request = new HttpGet(url.toExternalForm()); - } else if (method.equalsIgnoreCase("PUT")) { - request = new HttpPut(url.toExternalForm()); - } else if (method.equalsIgnoreCase("POST")) { - request = new HttpPost(url.toExternalForm()); - } - - // if the URL contains user info AND if this a DefaultHttpClient - // then preemptively set the auth credentials - if (url.getUserInfo() != null) { - if (url.getUserInfo().contains(":")) { - String[] userInfoSplit = url.getUserInfo().split(":"); - final Credentials creds = new UsernamePasswordCredentials( - userInfoSplit[0], userInfoSplit[1]); - if (httpClient instanceof DefaultHttpClient) { - DefaultHttpClient dhc = (DefaultHttpClient) httpClient; - - HttpRequestInterceptor preemptiveAuth = new HttpRequestInterceptor() { - - @Override - public void process(HttpRequest request, - HttpContext context) throws HttpException, - IOException { - AuthState authState = (AuthState) context - .getAttribute(ClientContext.TARGET_AUTH_STATE); - CredentialsProvider credsProvider = (CredentialsProvider) context - .getAttribute(ClientContext.CREDS_PROVIDER); - HttpHost targetHost = (HttpHost) context - .getAttribute(ExecutionContext.HTTP_TARGET_HOST); - - if (authState.getAuthScheme() == null) { - AuthScope authScope = new AuthScope( - targetHost.getHostName(), - targetHost.getPort()); - authState.setAuthScheme(new BasicScheme()); - authState.setCredentials(creds); - } - } - }; - - dhc.addRequestInterceptor(preemptiveAuth, 0); - } - } else { - Log.w(TDDatabase.TAG, - "Unable to parse user info, not setting credentials"); - } - } - - request.addHeader("Accept", "application/json"); - - // set body if appropriate - if (body != null && request instanceof HttpEntityEnclosingRequestBase) { - byte[] bodyBytes = null; - try { - bodyBytes = TDServer.getObjectMapper().writeValueAsBytes(body); - } catch (Exception e) { - Log.e(TDDatabase.TAG, "Error serializing body of request", e); - } - ByteArrayEntity entity = new ByteArrayEntity(bodyBytes); - entity.setContentType("application/json"); - ((HttpEntityEnclosingRequestBase) request).setEntity(entity); - } - - Object fullBody = null; - Throwable error = null; - try { - HttpResponse response = httpClient.execute(request); - StatusLine status = response.getStatusLine(); - if (status.getStatusCode() >= 300) { - Log.e(TDDatabase.TAG, - "Got error " + Integer.toString(status.getStatusCode())); - Log.e(TDDatabase.TAG, "Request was for: " + request.toString()); - Log.e(TDDatabase.TAG, - "Status reason: " + status.getReasonPhrase()); - error = new HttpResponseException(status.getStatusCode(), - status.getReasonPhrase()); - } else { - HttpEntity temp = response.getEntity(); - if (temp != null) { - try { - InputStream stream = temp.getContent(); - fullBody = TDServer.getObjectMapper().readValue(stream, - Object.class); - } finally { - try { - temp.consumeContent(); - } catch (IOException e) { - } - } - } - } - } catch (ClientProtocolException e) { - Log.e(TDDatabase.TAG, "client protocol exception", e); - error = e; - } catch (IOException e) { - Log.e(TDDatabase.TAG, "io exception", e); - error = e; - } - respondWithResult(fullBody, error); - } - - public void respondWithResult(final Object result, final Throwable error) { - if (workExecutor != null) { - workExecutor.submit(new Runnable() { - - @Override - public void run() { - try { - onCompletion.onCompletion(result, error); - } catch (Exception e) { - // don't let this crash the thread - Log.e(TDDatabase.TAG, - "TDRemoteRequestCompletionBlock throw Exception", - e); - } - } - }); - } else { - Log.e(TDDatabase.TAG, "work executor was null!!!"); - } - } + private ScheduledExecutorService workExecutor; + private final HttpClientFactory clientFactory; + private String method; + private URL url; + private Map headers; + private Object body; + private TDRemoteRequestCompletionBlock onCompletion; + + public TDRemoteRequest(ScheduledExecutorService workExecutor, + HttpClientFactory clientFactory, String method, URL url, + Map headers, Object body, + TDRemoteRequestCompletionBlock onCompletion) { + this.clientFactory = clientFactory; + this.method = method; + this.url = url; + this.headers = headers; + this.body = body; + this.onCompletion = onCompletion; + this.workExecutor = workExecutor; + } + + @Override + public void run() { + HttpClient httpClient = clientFactory.getHttpClient(); + ClientConnectionManager manager = httpClient.getConnectionManager(); + + HttpUriRequest request = null; + if (method.equalsIgnoreCase("GET")) { + request = new HttpGet(url.toExternalForm()); + } else if (method.equalsIgnoreCase("PUT")) { + request = new HttpPut(url.toExternalForm()); + } else if (method.equalsIgnoreCase("POST")) { + request = new HttpPost(url.toExternalForm()); + } + + // add headers + if(headers != null && headers.size() > 0) { + for(String key : headers.keySet()) { + request.addHeader(key, headers.get(key)); + } + } + + // if the URL contains user info AND if this a DefaultHttpClient + // then preemptively set the auth credentials + if (url.getUserInfo() != null) { + if (url.getUserInfo().contains(":")) { + String[] userInfoSplit = url.getUserInfo().split(":"); + final Credentials creds = new UsernamePasswordCredentials( + userInfoSplit[0], userInfoSplit[1]); + if (httpClient instanceof DefaultHttpClient) { + DefaultHttpClient dhc = (DefaultHttpClient) httpClient; + + HttpRequestInterceptor preemptiveAuth = new HttpRequestInterceptor() { + + @Override + public void process(HttpRequest request, + HttpContext context) throws HttpException, + IOException { + AuthState authState = (AuthState) context + .getAttribute(ClientContext.TARGET_AUTH_STATE); + CredentialsProvider credsProvider = (CredentialsProvider) context + .getAttribute(ClientContext.CREDS_PROVIDER); + HttpHost targetHost = (HttpHost) context + .getAttribute(ExecutionContext.HTTP_TARGET_HOST); + + if (authState.getAuthScheme() == null) { + AuthScope authScope = new AuthScope( + targetHost.getHostName(), + targetHost.getPort()); + authState.setAuthScheme(new BasicScheme()); + authState.setCredentials(creds); + } + } + }; + + dhc.addRequestInterceptor(preemptiveAuth, 0); + } + } else { + Log.w(TDDatabase.TAG, + "Unable to parse user info, not setting credentials"); + } + } + + request.addHeader("Accept", "application/json"); + + // set body if appropriate + if (body != null && request instanceof HttpEntityEnclosingRequestBase) { + byte[] bodyBytes = null; + try { + bodyBytes = TDServer.getObjectMapper().writeValueAsBytes(body); + } catch (Exception e) { + Log.e(TDDatabase.TAG, "Error serializing body of request", e); + } + ByteArrayEntity entity = new ByteArrayEntity(bodyBytes); + entity.setContentType("application/json"); + ((HttpEntityEnclosingRequestBase) request).setEntity(entity); + } + + Object fullBody = null; + Throwable error = null; + try { + HttpResponse response = httpClient.execute(request); + StatusLine status = response.getStatusLine(); + if (status.getStatusCode() >= 300) { + Log.e(TDDatabase.TAG, + "Got error " + Integer.toString(status.getStatusCode())); + Log.e(TDDatabase.TAG, "Request was for: " + request.toString()); + Log.e(TDDatabase.TAG, + "Status reason: " + status.getReasonPhrase()); + error = new HttpResponseException(status.getStatusCode(), + status.getReasonPhrase()); + } else { + HttpEntity temp = response.getEntity(); + if (temp != null) { + try { + InputStream stream = temp.getContent(); + fullBody = TDServer.getObjectMapper().readValue(stream, + Object.class); + } finally { + try { + temp.consumeContent(); + } catch (IOException e) { + } + } + } + } + } catch (ClientProtocolException e) { + Log.e(TDDatabase.TAG, "client protocol exception", e); + error = e; + } catch (IOException e) { + Log.e(TDDatabase.TAG, "io exception", e); + error = e; + } + respondWithResult(fullBody, error); + } + + public void respondWithResult(final Object result, final Throwable error) { + if (workExecutor != null) { + workExecutor.submit(new Runnable() { + + @Override + public void run() { + try { + onCompletion.onCompletion(result, error); + } catch (Exception e) { + // don't let this crash the thread + Log.e(TDDatabase.TAG, + "TDRemoteRequestCompletionBlock throw Exception", + e); + } + } + }); + } else { + Log.e(TDDatabase.TAG, "work executor was null!!!"); + } + } }