public void TestPulledChangesAreExternal() { var changeNotifications = 0; EventHandler <DatabaseChangeEventArgs> handler = (sender, e) => { changeNotifications++; Assert.IsTrue(e.IsExternal); }; database.Changed += handler; // Insert a dcoument as if it came from a remote source. var rev = new RevisionInternal("docId", "1-abcd", false); var properties = new Dictionary <string, object>(); properties["_id"] = rev.GetDocId(); properties["_rev"] = rev.GetRevId(); rev.SetProperties(properties); var history = new List <string>(); history.Add(rev.GetRevId()); database.ForceInsert(rev, history, GetReplicationURL()); Assert.AreEqual(1, changeNotifications); // Analysis disable once DelegateSubtraction database.Changed -= handler; }
public void TestPulledChangesAreExternal() { var countDown = new CountdownEvent(1); EventHandler <DatabaseChangeEventArgs> handler = (sender, e) => { countDown.Signal(); Assert.IsFalse(e.IsExternal); }; database.Changed += handler; // Insert a dcoument as if it came from a remote source. var rev = new RevisionInternal("docId", "1-abcd".AsRevID(), false); var properties = new Dictionary <string, object>(); properties.SetDocRevID(rev.DocID, rev.RevID); rev.SetProperties(properties); var history = new List <RevisionID>(); history.Add(rev.RevID); database.ForceInsert(rev, history, GetReplicationURL()); Assert.IsTrue(countDown.Wait(TimeSpan.FromSeconds(1))); // Analysis disable once DelegateSubtraction database.Changed -= handler; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal SavedRevision PutProperties(IDictionary <String, Object> properties, String prevID, Boolean allowConflict) { string newId = null; if (properties != null && properties.ContainsKey("_id")) { newId = (string)properties.Get("_id"); } if (newId != null && !newId.Equals(Id, StringComparison.InvariantCultureIgnoreCase)) { Log.W(Database.Tag, String.Format("Trying to put wrong _id to this: {0} properties: {1}", this, properties)); // TODO: Make sure all string formats use .NET codes, and not Java. } // Process _attachments dict, converting CBLAttachments to dicts: IDictionary <string, object> attachments = null; if (properties != null && properties.ContainsKey("_attachments")) { attachments = (IDictionary <string, object>)properties.Get("_attachments"); } if (attachments != null && attachments.Count > 0) { var updatedAttachments = Attachment.InstallAttachmentBodies(attachments, Database); properties["_attachments"] = updatedAttachments; } var hasTrueDeletedProperty = false; if (properties != null) { hasTrueDeletedProperty = properties.Get("_deleted") != null && ((bool)properties.Get("_deleted")); } var deleted = (properties == null) || hasTrueDeletedProperty; var rev = new RevisionInternal(Id, null, deleted, Database); if (deleted) { rev.SetJson(Encoding.UTF8.GetBytes("{}")); } if (properties != null) { rev.SetProperties(properties); } var newRev = Database.PutRevision(rev, prevID, allowConflict); if (newRev == null) { return(null); } return(new SavedRevision(this, newRev)); }
/// <summary> /// Test that the public API works as expected in change notifications after a rev tree /// insertion. /// </summary> /// <remarks> /// Test that the public API works as expected in change notifications after a rev tree /// insertion. See https://github.com/couchbase/couchbase-lite-android-core/pull/27 /// </remarks> /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestRevTreeChangeNotifications() { string DocumentId = "MyDocId"; // add a document with a single (first) revision RevisionInternal rev = new RevisionInternal(DocumentId, "1-one", false, database); IDictionary <string, object> revProperties = new Dictionary <string, object>(); revProperties.Put("_id", rev.GetDocId()); revProperties.Put("_rev", rev.GetRevId()); revProperties.Put("message", "hi"); rev.SetProperties(revProperties); IList <string> revHistory = Arrays.AsList(rev.GetRevId()); Database.ChangeListener listener = new _ChangeListener_154(this, DocumentId, rev); database.AddChangeListener(listener); database.ForceInsert(rev, revHistory, null); database.RemoveChangeListener(listener); // add two more revisions to the document RevisionInternal rev3 = new RevisionInternal(DocumentId, "3-three", false, database ); IDictionary <string, object> rev3Properties = new Dictionary <string, object>(); rev3Properties.Put("_id", rev3.GetDocId()); rev3Properties.Put("_rev", rev3.GetRevId()); rev3Properties.Put("message", "hi again"); rev3.SetProperties(rev3Properties); IList <string> rev3History = Arrays.AsList(rev3.GetRevId(), "2-two", rev.GetRevId( )); listener = new _ChangeListener_182(this, DocumentId, rev3); database.AddChangeListener(listener); database.ForceInsert(rev3, rev3History, null); database.RemoveChangeListener(listener); // add a conflicting revision, with the same history length as the last revision we // inserted. Since this new revision's revID has a higher ASCII sort, it should become the // new winning revision. RevisionInternal conflictRev = new RevisionInternal(DocumentId, "3-winner", false , database); IDictionary <string, object> conflictProperties = new Dictionary <string, object>(); conflictProperties.Put("_id", conflictRev.GetDocId()); conflictProperties.Put("_rev", conflictRev.GetRevId()); conflictProperties.Put("message", "winner"); conflictRev.SetProperties(conflictProperties); IList <string> conflictRevHistory = Arrays.AsList(conflictRev.GetRevId(), "2-two", rev.GetRevId()); listener = new _ChangeListener_217(this, DocumentId, conflictRev); database.AddChangeListener(listener); database.ForceInsert(conflictRev, conflictRevHistory, null); database.RemoveChangeListener(listener); }
public void TestForceInsertEmptyHistory() { var rev = new RevisionInternal("FakeDocId", "1-abcd".AsRevID(), false); var revProperties = new Dictionary <string, object>(); revProperties.SetDocRevID(rev.DocID, rev.RevID); revProperties["message"] = "hi"; rev.SetProperties(revProperties); IList <RevisionID> revHistory = null; database.ForceInsert(rev, revHistory, null); }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestForceInsertEmptyHistory() { IList <string> revHistory = null; RevisionInternal rev = new RevisionInternal("FakeDocId", "1-tango", false, database ); IDictionary <string, object> revProperties = new Dictionary <string, object>(); revProperties.Put("_id", rev.GetDocId()); revProperties.Put("_rev", rev.GetRevId()); revProperties["message"] = "hi"; rev.SetProperties(revProperties); database.ForceInsert(rev, revHistory, null); }
internal SavedRevision PutProperties(IDictionary <string, object> properties, string prevID, bool allowConflict) { string newId = null; if (properties != null && properties.ContainsKey("_id")) { newId = (string)properties.Get("_id"); } if (newId != null && !Sharpen.Runtime.EqualsIgnoreCase(newId, GetId())) { Log.W(Database.Tag, string.Format("Trying to put wrong _id to this: %s properties: %s" , this, properties)); } // Process _attachments dict, converting CBLAttachments to dicts: IDictionary <string, object> attachments = null; if (properties != null && properties.ContainsKey("_attachments")) { attachments = (IDictionary <string, object>)properties.Get("_attachments"); } if (attachments != null && attachments.Count > 0) { IDictionary <string, object> updatedAttachments = Attachment.InstallAttachmentBodies (attachments, database); properties.Put("_attachments", updatedAttachments); } bool hasTrueDeletedProperty = false; if (properties != null) { hasTrueDeletedProperty = properties.Get("_deleted") != null && ((bool)properties. Get("_deleted")); } bool deleted = (properties == null) || hasTrueDeletedProperty; RevisionInternal rev = new RevisionInternal(documentId, null, deleted, database); if (properties != null) { rev.SetProperties(properties); } RevisionInternal newRev = database.PutRevision(rev, prevID, allowConflict); if (newRev == null) { return(null); } return(new SavedRevision(this, newRev)); }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestPulledChangesAreExternal() { changeNotifications = 0; Database.ChangeListener changeListener = new _ChangeListener_61(this); database.AddChangeListener(changeListener); // Insert a document as if it came from a remote source. RevisionInternal rev = new RevisionInternal("docId", "1-rev", false, database); IDictionary <string, object> properties = new Dictionary <string, object>(); properties.Put("_id", rev.GetDocId()); properties.Put("_rev", rev.GetRevId()); rev.SetProperties(properties); database.ForceInsert(rev, Arrays.AsList(rev.GetRevId()), GetReplicationURL()); // Make sure that the assertion in changeListener was called. NUnit.Framework.Assert.AreEqual(1, changeNotifications); }
/// <summary> /// Adds an existing revision copied from another database. Unlike a normal insertion, this does /// not assign a new revision ID; instead the revision's ID must be given. Ths revision's history /// (ancestry) must be given, which can put it anywhere in the revision tree. It's not an error if /// the revision already exists locally; it will just be ignored. /// /// This is not an operation that clients normall perform; it's used by the replicator. You might want /// to use it if you're pre-loading a database with canned content, or if you're implementing some new /// kind of replicator that transfers revisions from another database /// </summary> /// <param name="properties">The properties of the revision (_id and _rev will be ignored but _deleted /// and _attachments are recognized)</param> /// <param name="attachments">A dictionary providing attachment bodies. The keys are the attachment /// names (matching the keys in the properties `_attachments` dictionary) and the values are streams /// that contain the attachment bodies.</param> /// <param name="revisionHistory">The revision history in the form of an array of revision-ID strings, in /// reverse chronological order. The first item must be the new revision's ID. Following items are its /// parent's ID, etc.</param> /// <param name="sourceUri">The URL of the database this revision came from, if any. (This value /// shows up in the Database Changed event triggered by this insertion, and can help clients decide /// whether the change is local or not)</param> /// <returns><c>true</c> on success, false otherwise</returns> public bool PutExistingRevision(IDictionary <string, object> properties, IDictionary <string, Stream> attachments, IList <string> revisionHistory, Uri sourceUri) { if (revisionHistory == null || revisionHistory.Count == 0) { Log.To.Database.E(Tag, "Invalid revision history in PutExistingRevision (must contain at " + "least one revision ID), throwing..."); throw new ArgumentException("revisionHistory"); } var revIDs = revisionHistory.AsRevIDs().ToList(); var rev = new RevisionInternal(Id, revIDs[0], properties.CblDeleted()); rev.SetProperties(PropertiesToInsert(properties)); if (!Database.RegisterAttachmentBodies(attachments, rev)) { Log.To.Database.W(Tag, "Failed to register attachment bodies, aborting insert..."); return(false); } Database.ForceInsert(rev, revIDs, sourceUri); return(true); }
public RevisionInternal GetLocalDocument(string docId, string revId) { if (!docId.StartsWith("_local/")) { return(null); } var retVal = default(RevisionInternal); WithC4Raw(docId, "_local", doc => { if (doc == null) { return; } var gotRevId = (string)doc->meta; if (revId != null && revId != gotRevId || doc->body.size == 0) { return; } var properties = default(IDictionary <string, object>); try { properties = Manager.GetObjectMapper().ReadValue <IDictionary <string, object> >(doc->body); } catch (CouchbaseLiteException) { Log.W(TAG, "Invalid JSON for document {0}", docId); return; } properties["_id"] = docId; properties["_rev"] = gotRevId; retVal = new RevisionInternal(docId, revId, false); retVal.SetProperties(properties); }); return(retVal); }
public void TestRevTreeChangeNotification() { const string DOCUMENT_ID = "MyDocId"; var rev = new RevisionInternal(DOCUMENT_ID, "1-one", false, database); var revProperties = new Dictionary <string, object>(); revProperties["_id"] = rev.GetDocId(); revProperties["_rev"] = rev.GetRevId(); revProperties["message"] = "hi"; rev.SetProperties(revProperties); var revHistory = new List <string>(); revHistory.Add(rev.GetRevId()); EventHandler <DatabaseChangeEventArgs> handler = (sender, e) => { var changes = e.Changes.ToList(); Assert.AreEqual(1, changes.Count); var change = changes[0]; Assert.AreEqual(DOCUMENT_ID, change.DocumentId); Assert.AreEqual(rev.GetRevId(), change.RevisionId); Assert.IsTrue(change.IsCurrentRevision); Assert.IsFalse(change.IsConflict); var current = database.GetDocument(change.DocumentId).CurrentRevision; Assert.AreEqual(rev.GetRevId(), current.Id); }; database.Changed += handler; database.ForceInsert(rev, revHistory, null); database.Changed -= handler; // add two more revisions to the document var rev3 = new RevisionInternal(DOCUMENT_ID, "3-three", false, database); var rev3Properties = new Dictionary <string, object>(); rev3Properties["_id"] = rev3.GetDocId(); rev3Properties["_rev"] = rev3.GetRevId(); rev3Properties["message"] = "hi again"; rev3.SetProperties(rev3Properties); var rev3History = new List <string>(); rev3History.Add(rev3.GetRevId()); rev3History.Add("2-two"); rev3History.Add(rev.GetRevId()); handler = (sender, e) => { var changes = e.Changes.ToList(); Assert.AreEqual(1, changes.Count); var change = changes[0]; Assert.AreEqual(DOCUMENT_ID, change.DocumentId); Assert.AreEqual(rev3.GetRevId(), change.RevisionId); Assert.IsTrue(change.IsCurrentRevision); Assert.IsFalse(change.IsConflict); var doc = database.GetDocument(change.DocumentId); Assert.AreEqual(rev3.GetRevId(), doc.CurrentRevisionId); try { Assert.AreEqual(3, doc.RevisionHistory.ToList().Count); } catch (CouchbaseLiteException ex) { Assert.Fail(); } }; database.Changed += handler; database.ForceInsert(rev3, rev3History, null); database.Changed -= handler; // add a conflicting revision, with the same history length as the last revision we // inserted. Since this new revision's revID has a higher ASCII sort, it should become the // new winning revision. var conflictRev = new RevisionInternal(DOCUMENT_ID, "3-winner", false, database); var conflictProperties = new Dictionary <string, object>(); conflictProperties["_id"] = conflictRev.GetDocId(); conflictProperties["_rev"] = conflictRev.GetRevId(); conflictProperties["message"] = "winner"; conflictRev.SetProperties(conflictProperties); var conflictRevHistory = new List <string>(); conflictRevHistory.Add(conflictRev.GetRevId()); conflictRevHistory.Add("2-two"); conflictRevHistory.Add(rev.GetRevId()); handler = (sender, e) => { var changes = e.Changes.ToList(); Assert.AreEqual(1, changes.Count); var change = changes[0]; Assert.AreEqual(DOCUMENT_ID, change.DocumentId); Assert.AreEqual(conflictRev.GetRevId(), change.RevisionId); Assert.IsTrue(change.IsCurrentRevision); Assert.IsFalse(change.IsConflict); var doc = database.GetDocument(change.DocumentId); Assert.AreEqual(rev3.GetRevId(), doc.CurrentRevisionId); try { Assert.AreEqual(2, doc.ConflictingRevisions.ToList().Count); Assert.AreEqual(3, doc.RevisionHistory.ToList().Count); } catch (CouchbaseLiteException ex) { Assert.Fail(); } }; database.Changed += handler; database.ForceInsert(conflictRev, conflictRevHistory, null); database.Changed -= handler; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestValidations() { Validator validator = new _Validator_19(this); database.SetValidation("hoopy", validator); // POST a valid new document: IDictionary <string, object> props = new Dictionary <string, object>(); props.Put("name", "Zaphod Beeblebrox"); props.Put("towel", "velvet"); RevisionInternal rev = new RevisionInternal(props, database); Status status = new Status(); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); // PUT a valid update: props.Put("head_count", 3); rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); // PUT an invalid update: Sharpen.Collections.Remove(props, "towel"); rev.SetProperties(props); validationCalled = false; bool gotExpectedError = false; try { rev = database.PutRevision(rev, rev.GetRevId(), false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == Status.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // POST an invalid new document: props = new Dictionary <string, object>(); props.Put("name", "Vogon"); props.Put("poetry", true); rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == Status.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // PUT a valid new document with an ID: props = new Dictionary <string, object>(); props.Put("_id", "ford"); props.Put("name", "Ford Prefect"); props.Put("towel", "terrycloth"); rev = new RevisionInternal(props, database); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual("ford", rev.GetDocId()); // DELETE a document: rev = new RevisionInternal(rev.GetDocId(), rev.GetRevId(), true, database); NUnit.Framework.Assert.IsTrue(rev.IsDeleted()); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); // PUT an invalid new document: props = new Dictionary <string, object>(); props.Put("_id", "petunias"); props.Put("name", "Pot of Petunias"); rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == Status.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); }
/// <summary>Fetches the contents of a revision from the remote db, including its parent revision ID. /// </summary> /// <remarks> /// Fetches the contents of a revision from the remote db, including its parent revision ID. /// The contents are stored into rev.properties. /// </remarks> internal void PullRemoteRevision(RevisionInternal rev) { Log.D(Tag, this + "|" + Thread.CurrentThread() + ": pullRemoteRevision with rev: " + rev); Log.D(Tag, this + "|" + Thread.CurrentThread() + ": pullRemoteRevision() calling asyncTaskStarted()"); 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 var path = new StringBuilder("/" + HttpUtility.UrlEncode(rev.GetDocId()) + "?rev=" + HttpUtility.UrlEncode(rev.GetRevId()) + "&revs=true&attachments=true"); var knownRevs = KnownCurrentRevIDs(rev); if (knownRevs == null) { //this means something is wrong, possibly the replicator has shut down Log.D(Tag, this + "|" + Thread.CurrentThread() + ": pullRemoteRevision() calling asyncTaskFinished()"); AsyncTaskFinished(1); httpConnectionCount--; return; } if (knownRevs.Count > 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 var pathInside = path.ToString(); SendAsyncMultipartDownloaderRequest(HttpMethod.Get, pathInside, null, LocalDatabase, (result, e) => { try { // OK, now we've got the response revision: Log.D(Tag, this + ": pullRemoteRevision got response for rev: " + rev); if (result != null) { var properties = ((JObject)result).ToObject <IDictionary <string, object> >(); var history = Database.ParseCouchDBRevisionHistory(properties); if (history != null) { rev.SetProperties(properties); // Add to batcher ... eventually it will be fed to -insertRevisions:. var toInsert = new AList <object> (); toInsert.AddItem(rev); toInsert.AddItem(history); Log.D(Tag, this + ": pullRemoteRevision add rev: " + rev + " to batcher"); downloadsToInsert.QueueObject(toInsert); Log.D(Tag, this + "|" + Thread.CurrentThread() + ": pullRemoteRevision.onCompletion() calling asyncTaskStarted()"); AsyncTaskStarted(); } else { Log.W(Tag, this + ": Missing revision history in response from " + pathInside); CompletedChangesCount += 1; } } else { if (e != null) { Log.E(Tag, "Error pulling remote revision", e); LastError = e; } CompletedChangesCount += 1; } } finally { Log.D(Tag, this + "|" + Thread.CurrentThread() + ": pullRemoteRevision.onCompletion() calling asyncTaskFinished()"); AsyncTaskFinished(1); } // Note that we've finished this task; then start another one if there // are still revisions waiting to be pulled: --httpConnectionCount; PullRemoteRevisions(); }); }
public void TestRevTree() { var rev = new RevisionInternal("MyDocId", "4-abcd", false); var revProperties = new Dictionary <string, object>(); revProperties["_id"] = rev.DocID; revProperties["_rev"] = rev.RevID; revProperties["message"] = "hi"; rev.SetProperties(revProperties); var revHistory = new List <string>(); revHistory.Add(rev.RevID); revHistory.Add("3-abcd"); revHistory.Add("2-abcd"); revHistory.Add("1-abcd"); database.ForceInsert(rev, revHistory, null); Assert.AreEqual(1, database.GetDocumentCount()); VerifyHistory(database, rev, revHistory); var conflict = new RevisionInternal("MyDocId", "5-abcd", false); var conflictProperties = new Dictionary <string, object>(); conflictProperties["_id"] = conflict.DocID; conflictProperties["_rev"] = conflict.RevID; conflictProperties["message"] = "yo"; conflict.SetProperties(conflictProperties); var conflictHistory = new List <string>(); conflictHistory.Add(conflict.RevID); conflictHistory.Add("4-bcde"); conflictHistory.Add("3-bcde"); conflictHistory.Add("2-abcd"); conflictHistory.Add("1-abcd"); database.ForceInsert(conflict, conflictHistory, null); Assert.AreEqual(1, database.GetDocumentCount()); VerifyHistory(database, conflict, conflictHistory); // Add an unrelated document: var other = new RevisionInternal("AnotherDocID", "1-cdef", false); var otherProperties = new Dictionary <string, object>(); otherProperties["language"] = "jp"; other.SetProperties(otherProperties); var otherHistory = new List <string>(); otherHistory.Add(other.RevID); database.ForceInsert(other, otherHistory, null); // Fetch one of those phantom revisions with no body: var rev2 = database.GetDocument(rev.DocID, "2-abcd", true); Assert.IsNull(rev2); // Make sure no duplicate rows were inserted for the common revisions: Assert.IsTrue(database.GetLastSequenceNumber() <= 8); // Make sure the revision with the higher revID wins the conflict: var current = database.GetDocument(rev.DocID, null, true); Assert.AreEqual(conflict, current); // Get the _changes feed and verify only the winner is in it: var options = new ChangesOptions(); var changes = database.ChangesSince(0, options, null, null); var expectedChanges = new RevisionList(); expectedChanges.Add(conflict); expectedChanges.Add(other); Assert.AreEqual(expectedChanges, changes); options.IncludeConflicts = true; changes = database.ChangesSince(0, options, null, null); expectedChanges = new RevisionList(); expectedChanges.Add(rev); expectedChanges.Add(conflict); expectedChanges.Add(other); var expectedChangesAlt = new RevisionList(); expectedChangesAlt.Add(conflict); expectedChangesAlt.Add(rev); expectedChangesAlt.Add(other); Assert.IsTrue(expectedChanges.SequenceEqual(changes) || expectedChangesAlt.SequenceEqual(changes)); }
public void TestValidations() { ValidateDelegate validator = (newRevision, context) => { Assert.IsNotNull(newRevision); Assert.IsNotNull(context); Assert.IsTrue(newRevision.Properties != null || newRevision.IsDeletion); validationCalled = true; bool hoopy = newRevision.IsDeletion || (newRevision.Properties.Get("towel") != null); Log.V(ValidationsTest.Tag, string.Format("--- Validating {0} --> {1}", newRevision.Properties, hoopy)); if (!hoopy) { context.Reject("Where's your towel?"); } return(hoopy); }; database.SetValidation("hoopy", validator); // POST a valid new document: IDictionary <string, object> props = new Dictionary <string, object>(); props["name"] = "Zaphod Beeblebrox"; props["towel"] = "velvet"; RevisionInternal rev = new RevisionInternal(props, database); Status status = new Status(); validationCalled = false; rev = database.PutRevision(rev, null, false, status); Assert.IsTrue(validationCalled); Assert.AreEqual(StatusCode.Created, status.GetCode()); // PUT a valid update: props["head_count"] = 3; rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); Assert.IsTrue(validationCalled); Assert.AreEqual(StatusCode.Created, status.GetCode()); // PUT an invalid update: Sharpen.Collections.Remove(props, "towel"); rev.SetProperties(props); validationCalled = false; bool gotExpectedError = false; try { rev = database.PutRevision(rev, rev.GetRevId(), false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } Assert.IsTrue(validationCalled); Assert.IsTrue(gotExpectedError); // POST an invalid new document: props = new Dictionary <string, object>(); props["name"] = "Vogon"; props["poetry"] = true; rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } Assert.IsTrue(validationCalled); Assert.IsTrue(gotExpectedError); // PUT a valid new document with an ID: props = new Dictionary <string, object>(); props["_id"] = "ford"; props["name"] = "Ford Prefect"; props["towel"] = "terrycloth"; rev = new RevisionInternal(props, database); validationCalled = false; rev = database.PutRevision(rev, null, false, status); Assert.IsTrue(validationCalled); Assert.AreEqual("ford", rev.GetDocId()); // DELETE a document: rev = new RevisionInternal(rev.GetDocId(), rev.GetRevId(), true, database); Assert.IsTrue(rev.IsDeleted()); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); Assert.IsTrue(validationCalled); // PUT an invalid new document: props = new Dictionary <string, object>(); props["_id"] = "petunias"; props["name"] = "Pot of Petunias"; rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } Assert.IsTrue(validationCalled); Assert.IsTrue(gotExpectedError); }
public void TestViewIndex() { int numTimesMapFunctionInvoked = 0; var dict1 = new Dictionary <string, object>(); dict1["key"] = "one"; var dict2 = new Dictionary <string, object>(); dict2["key"] = "two"; var dict3 = new Dictionary <string, object>(); dict3["key"] = "three"; var dictX = new Dictionary <string, object>(); dictX["clef"] = "quatre"; var rev1 = PutDoc(database, dict1); var rev2 = PutDoc(database, dict2); var rev3 = PutDoc(database, dict3); PutDoc(database, dictX); var view = database.GetView("aview"); var numTimesInvoked = 0; MapDelegate mapBlock = (document, emitter) => { numTimesInvoked += 1; Assert.IsNotNull(document["_id"]); Assert.IsNotNull(document["_rev"]); if (document.ContainsKey("key") && document["key"] != null) { emitter(document["key"], null); } }; view.SetMap(mapBlock, "1"); Assert.AreEqual(1, view.Id); Assert.IsTrue(view.IsStale); view.UpdateIndex(); IList <IDictionary <string, object> > dumpResult = view.Dump(); Log.V(Tag, "View dump: " + dumpResult); Assert.AreEqual(3, dumpResult.Count); Assert.AreEqual("\"one\"", dumpResult[0]["key"]); Assert.AreEqual(1, dumpResult[0]["seq"]); Assert.AreEqual("\"two\"", dumpResult[2]["key"]); Assert.AreEqual(2, dumpResult[2]["seq"]); Assert.AreEqual("\"three\"", dumpResult[1]["key"]); Assert.AreEqual(3, dumpResult[1]["seq"]); //no-op reindex Assert.IsFalse(view.IsStale); view.UpdateIndex(); // Now add a doc and update a doc: var threeUpdated = new RevisionInternal(rev3.GetDocId(), rev3.GetRevId(), false, database); numTimesMapFunctionInvoked = numTimesInvoked; var newdict3 = new Dictionary <string, object>(); newdict3["key"] = "3hree"; threeUpdated.SetProperties(newdict3); Status status = new Status(); rev3 = database.PutRevision(threeUpdated, rev3.GetRevId(), false, status); Assert.IsTrue(status.IsSuccessful); // Reindex again: Assert.IsTrue(view.IsStale); view.UpdateIndex(); // Make sure the map function was only invoked one more time (for the document that was added) Assert.AreEqual(numTimesInvoked, numTimesMapFunctionInvoked + 1); var dict4 = new Dictionary <string, object>(); dict4["key"] = "four"; var rev4 = PutDoc(database, dict4); var twoDeleted = new RevisionInternal(rev2.GetDocId(), rev2.GetRevId(), true, database); database.PutRevision(twoDeleted, rev2.GetRevId(), false, status); Assert.IsTrue(status.IsSuccessful); // Reindex again: Assert.IsTrue(view.IsStale); view.UpdateIndex(); dumpResult = view.Dump(); Log.V(Tag, "View dump: " + dumpResult); Assert.AreEqual(3, dumpResult.Count); Assert.AreEqual("\"one\"", dumpResult[2]["key"]); Assert.AreEqual(1, dumpResult[2]["seq"]); Assert.AreEqual("\"3hree\"", dumpResult[0]["key"]); Assert.AreEqual(5, dumpResult[0]["seq"]); Assert.AreEqual("\"four\"", dumpResult[1]["key"]); Assert.AreEqual(6, dumpResult[1]["seq"]); // Now do a real query: IList <QueryRow> rows = view.QueryWithOptions(null).ToList(); Assert.AreEqual(3, rows.Count); Assert.AreEqual("one", rows[2].Key); Assert.AreEqual(rev1.GetDocId(), rows[2].DocumentId); Assert.AreEqual("3hree", rows[0].Key); Assert.AreEqual(rev3.GetDocId(), rows[0].DocumentId); Assert.AreEqual("four", rows[1].Key); Assert.AreEqual(rev4.GetDocId(), rows[1].DocumentId); view.DeleteIndex(); }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestRevTree() { RevisionInternal rev = new RevisionInternal("MyDocId", "4-foxy", false, database); IDictionary <string, object> revProperties = new Dictionary <string, object>(); revProperties.Put("_id", rev.GetDocId()); revProperties.Put("_rev", rev.GetRevId()); revProperties["message"] = "hi"; rev.SetProperties(revProperties); IList <string> revHistory = new AList <string>(); revHistory.AddItem(rev.GetRevId()); revHistory.AddItem("3-thrice"); revHistory.AddItem("2-too"); revHistory.AddItem("1-won"); database.ForceInsert(rev, revHistory, null); NUnit.Framework.Assert.AreEqual(1, database.DocumentCount); VerifyHistory(database, rev, revHistory); RevisionInternal conflict = new RevisionInternal("MyDocId", "5-epsilon", false, database ); IDictionary <string, object> conflictProperties = new Dictionary <string, object>(); conflictProperties.Put("_id", conflict.GetDocId()); conflictProperties.Put("_rev", conflict.GetRevId()); conflictProperties["message"] = "yo"; conflict.SetProperties(conflictProperties); IList <string> conflictHistory = new AList <string>(); conflictHistory.AddItem(conflict.GetRevId()); conflictHistory.AddItem("4-delta"); conflictHistory.AddItem("3-gamma"); conflictHistory.AddItem("2-too"); conflictHistory.AddItem("1-won"); database.ForceInsert(conflict, conflictHistory, null); NUnit.Framework.Assert.AreEqual(1, database.DocumentCount); VerifyHistory(database, conflict, conflictHistory); // Add an unrelated document: RevisionInternal other = new RevisionInternal("AnotherDocID", "1-ichi", false, database ); IDictionary <string, object> otherProperties = new Dictionary <string, object>(); otherProperties["language"] = "jp"; other.SetProperties(otherProperties); IList <string> otherHistory = new AList <string>(); otherHistory.AddItem(other.GetRevId()); database.ForceInsert(other, otherHistory, null); // Fetch one of those phantom revisions with no body: RevisionInternal rev2 = database.GetDocumentWithIDAndRev(rev.GetDocId(), "2-too", EnumSet.NoneOf <TDContentOptions>()); NUnit.Framework.Assert.AreEqual(rev.GetDocId(), rev2.GetDocId()); NUnit.Framework.Assert.AreEqual("2-too", rev2.GetRevId()); //Assert.assertNull(rev2.getContent()); // Make sure no duplicate rows were inserted for the common revisions: NUnit.Framework.Assert.AreEqual(8, database.GetLastSequenceNumber()); // Make sure the revision with the higher revID wins the conflict: RevisionInternal current = database.GetDocumentWithIDAndRev(rev.GetDocId(), null, EnumSet.NoneOf <TDContentOptions>()); NUnit.Framework.Assert.AreEqual(conflict, current); // Get the _changes feed and verify only the winner is in it: ChangesOptions options = new ChangesOptions(); RevisionList changes = database.ChangesSince(0, options, null); RevisionList expectedChanges = new RevisionList(); expectedChanges.AddItem(conflict); expectedChanges.AddItem(other); NUnit.Framework.Assert.AreEqual(changes, expectedChanges); options.SetIncludeConflicts(true); changes = database.ChangesSince(0, options, null); expectedChanges = new RevisionList(); expectedChanges.AddItem(rev); expectedChanges.AddItem(conflict); expectedChanges.AddItem(other); NUnit.Framework.Assert.AreEqual(changes, expectedChanges); }
public void TestValidations() { ValidateDelegate validator = (newRevision, context) => { Assert.IsNotNull(newRevision); Assert.IsNotNull(context); Assert.IsTrue(newRevision.Properties != null || newRevision.IsDeletion); validationCalled = true; bool hoopy = newRevision.IsDeletion || (newRevision.Properties.Get("towel") != null); Console.WriteLine("--- Validating {0} --> {1}", newRevision.Properties, hoopy); if (!hoopy) { context.Reject("Where's your towel?"); } return(hoopy); }; database.SetValidation("hoopy", validator); // POST a valid new document: IDictionary <string, object> props = new Dictionary <string, object>(); props["name"] = "Zaphod Beeblebrox"; props["towel"] = "velvet"; RevisionInternal rev = new RevisionInternal(props); validationCalled = false; rev = database.PutRevision(rev, null, false); Assert.IsTrue(validationCalled); // PUT a valid update: props["head_count"] = 3; rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.RevID, false); Assert.IsTrue(validationCalled); // PUT an invalid update: props.Remove("towel"); rev.SetProperties(props); validationCalled = false; Assert.Throws <CouchbaseLiteException>(() => rev = database.PutRevision(rev, rev.RevID, false)); Assert.IsTrue(validationCalled); // POST an invalid new document: props = new Dictionary <string, object>(); props["name"] = "Vogon"; props["poetry"] = true; rev = new RevisionInternal(props); validationCalled = false; Assert.Throws <CouchbaseLiteException>(() => database.PutRevision(rev, null, false)); Assert.IsTrue(validationCalled); // PUT a valid new document with an ID: props = new Dictionary <string, object>(); props["_id"] = "ford"; props["name"] = "Ford Prefect"; props["towel"] = "terrycloth"; rev = new RevisionInternal(props); validationCalled = false; rev = database.PutRevision(rev, null, false); Assert.IsTrue(validationCalled); Assert.AreEqual("ford", rev.DocID); // DELETE a document: rev = new RevisionInternal(rev.DocID, rev.RevID, true); Assert.IsTrue(rev.Deleted); validationCalled = false; rev = database.PutRevision(rev, rev.RevID, false); Assert.IsTrue(validationCalled); // PUT an invalid new document: props = new Dictionary <string, object>(); props["_id"] = "petunias"; props["name"] = "Pot of Petunias"; rev = new RevisionInternal(props); validationCalled = false; Assert.Throws <CouchbaseLiteException>(() => rev = database.PutRevision(rev, null, false)); Assert.IsTrue(validationCalled); // Cancel the validation database.SetValidation("hoopy", null); validationCalled = false; Assert.DoesNotThrow(() => rev = database.PutRevision(rev, null, false)); Assert.IsFalse(validationCalled); }
public void TestRevTree() { var change = default(DocumentChange); database.Changed += (sender, args) => { Assert.AreEqual(1, args.Changes.Count()); Assert.IsNull(change, "Multiple notifications posted"); change = args.Changes.First(); }; var rev = new RevisionInternal("MyDocId", "4-4444".AsRevID(), false); var revProperties = new Dictionary <string, object>(); revProperties.SetDocRevID(rev.DocID, rev.RevID); revProperties["message"] = "hi"; rev.SetProperties(revProperties); var revHistory = new List <RevisionID>(); revHistory.Add(rev.RevID); revHistory.Add("3-3333".AsRevID()); revHistory.Add("2-2222".AsRevID()); revHistory.Add("1-1111".AsRevID()); database.ForceInsert(rev, revHistory, null); Assert.AreEqual(1, database.GetDocumentCount()); VerifyRev(rev, revHistory); Assert.AreEqual(Announcement(database, rev, rev), change); Assert.IsFalse(change.IsConflict); // No-op ForceInsert of already-existing revision var lastSeq = database.GetLastSequenceNumber(); database.ForceInsert(rev, revHistory, null); Assert.AreEqual(lastSeq, database.GetLastSequenceNumber()); var conflict = new RevisionInternal("MyDocId", "5-5555".AsRevID(), false); var conflictProperties = new Dictionary <string, object>(); conflictProperties.SetDocRevID(conflict.DocID, conflict.RevID); conflictProperties["message"] = "yo"; conflict.SetProperties(conflictProperties); var conflictHistory = new List <RevisionID>(); conflictHistory.Add(conflict.RevID); conflictHistory.Add("4-4545".AsRevID()); conflictHistory.Add("3-3030".AsRevID()); conflictHistory.Add("2-2222".AsRevID()); conflictHistory.Add("1-1111".AsRevID()); change = null; database.ForceInsert(conflict, conflictHistory, null); Assert.AreEqual(1, database.GetDocumentCount()); VerifyRev(conflict, conflictHistory); Assert.AreEqual(Announcement(database, conflict, conflict), change); Assert.IsTrue(change.IsConflict); // Add an unrelated document: var other = new RevisionInternal("AnotherDocID", "1-1010".AsRevID(), false); var otherProperties = new Dictionary <string, object>(); otherProperties["language"] = "jp"; other.SetProperties(otherProperties); var otherHistory = new List <RevisionID>(); otherHistory.Add(other.RevID); change = null; database.ForceInsert(other, otherHistory, null); Assert.AreEqual(Announcement(database, other, other), change); Assert.IsFalse(change.IsConflict); // Fetch one of those phantom revisions with no body: var rev2 = database.GetDocument(rev.DocID, "2-2222".AsRevID(), true); Assert.IsTrue(rev2.Missing); Assert.IsNull(rev2.GetBody()); Assert.IsNull(database.GetDocument(rev.DocID, "666-6666".AsRevID(), true)); // Make sure no duplicate rows were inserted for the common revisions: if (_storageType == StorageEngineTypes.SQLite) { Assert.AreEqual(8, database.GetLastSequenceNumber()); } else { Assert.AreEqual(3, database.GetLastSequenceNumber()); } // Make sure the revision with the higher revID wins the conflict: var current = database.GetDocument(rev.DocID, null, true); Assert.AreEqual(conflict, current); // Check that the list of conflicts is accurate var conflictingRevs = database.Storage.GetAllDocumentRevisions(rev.DocID, true); CollectionAssert.AreEqual(new[] { conflict, rev }, conflictingRevs); // Get the _changes feed and verify only the winner is in it: var options = new ChangesOptions(); var changes = database.ChangesSince(0, options, null, null); CollectionAssert.AreEqual(new[] { conflict, other }, changes); options.IncludeConflicts = true; changes = database.ChangesSince(0, options, null, null); var expectedChanges = new RevisionList(); expectedChanges.Add(rev); expectedChanges.Add(conflict); expectedChanges.Add(other); var expectedChangesAlt = new RevisionList(); expectedChangesAlt.Add(conflict); expectedChangesAlt.Add(rev); expectedChangesAlt.Add(other); Assert.IsTrue(expectedChanges.SequenceEqual(changes) || expectedChangesAlt.SequenceEqual(changes)); }
public RevisionInternal PutRevision(string inDocId, string inPrevRevId, IDictionary <string, object> properties, bool deleting, bool allowConflict, StoreValidation validationBlock) { if (_config.HasFlag(C4DatabaseFlags.ReadOnly)) { throw new CouchbaseLiteException("Attempting to write to a readonly database", StatusCode.Forbidden); } var json = default(string); if (properties != null) { json = Manager.GetObjectMapper().WriteValueAsString(Database.StripDocumentJSON(properties), true); } else { json = "{}"; } if (inDocId == null) { inDocId = Misc.CreateGUID(); } var putRev = default(RevisionInternal); var change = default(DocumentChange); var success = RunInTransaction(() => { var docId = inDocId; var prevRevId = inPrevRevId; var transactionSuccess = false; WithC4Document(docId, null, false, true, doc => { if (prevRevId != null) { // Updating an existing revision; make sure it exists and is a leaf: ForestDBBridge.Check(err => Native.c4doc_selectRevision(doc, prevRevId, false, err)); if (!allowConflict && !doc->selectedRev.IsLeaf) { throw new CouchbaseLiteException(StatusCode.Conflict); } } else { // No parent revision given: if (deleting) { // Didn't specify a revision to delete: NotFound or a Conflict, depending throw new CouchbaseLiteException(doc->Exists ? StatusCode.Conflict : StatusCode.NotFound); } // If doc exists, current rev must be in a deleted state or there will be a conflict: if (Native.c4doc_selectCurrentRevision(doc)) { if (doc->selectedRev.IsDeleted) { // New rev will be child of the tombstone: prevRevId = (string)doc->revID; } else { throw new CouchbaseLiteException(StatusCode.Conflict); } } } // Compute the new revID. (Can't be done earlier because prevRevID may have changed.) var newRevID = Delegate != null ? Delegate.GenerateRevID(Encoding.UTF8.GetBytes(json), deleting, prevRevId) : null; if (newRevID == null) { throw new CouchbaseLiteException(StatusCode.BadId); } putRev = new RevisionInternal(docId, newRevID, deleting); if (properties != null) { properties["_id"] = docId; properties["_rev"] = newRevID; putRev.SetProperties(properties); } // Run any validation blocks: if (validationBlock != null) { var prevRev = default(RevisionInternal); if (prevRevId != null) { prevRev = new RevisionInternal(docId, prevRevId, doc->selectedRev.IsDeleted); } var status = validationBlock(putRev, prevRev, prevRevId); if (status.IsError) { throw new CouchbaseLiteException(String.Format("{0} failed validation", putRev), status.Code); } } // Add the revision to the database: ForestDBBridge.Check(err => Native.c4doc_insertRevision(doc, newRevID, json, deleting, putRev.GetAttachments() != null, allowConflict, err)); var isWinner = SaveDocument(doc, newRevID, properties); putRev.SetSequence((long)doc->sequence); change = ChangeWithNewRevision(putRev, isWinner, doc, null); transactionSuccess = true; }); return(transactionSuccess); }); if (!success) { return(null); } if (Delegate != null && change != null) { Delegate.DatabaseStorageChanged(change); } return(putRev); }
/// <summary> /// Attempt to update a document based on the information in the HTTP request /// </summary> /// <returns>The resulting status of the operation</returns> /// <param name="context">The request context</param> /// <param name="db">The database in which the document exists</param> /// <param name="docId">The ID of the document being updated</param> /// <param name="body">The new document body</param> /// <param name="deleting">Whether or not the document is being deleted</param> /// <param name="allowConflict">Whether or not to allow a conflict to be inserted</param> /// <param name="outRev">The resulting revision of the document</param> public static StatusCode UpdateDocument(ICouchbaseListenerContext context, Database db, string docId, Body body, bool deleting, bool allowConflict, out RevisionInternal outRev) { outRev = null; if (body != null && !body.IsValidJSON()) { return(StatusCode.BadJson); } string prevRevId; if (!deleting) { var properties = body.GetProperties(); deleting = properties.GetCast <bool>("_deleted"); if (docId == null) { // POST's doc ID may come from the _id field of the JSON body. docId = properties.CblID(); if (docId == null && deleting) { return(StatusCode.BadId); } } // PUT's revision ID comes from the JSON body. prevRevId = properties.GetCast <string>("_rev"); } else { // DELETE's revision ID comes from the ?rev= query param prevRevId = context.GetQueryParam("rev"); } // A backup source of revision ID is an If-Match header: if (prevRevId == null) { prevRevId = context.IfMatch(); } if (docId == null && deleting) { return(StatusCode.BadId); } RevisionInternal rev = new RevisionInternal(docId, null, deleting); rev.SetBody(body); // Check for doc expiration var expirationTime = default(DateTime?); var tmp = default(object); var props = rev.GetProperties(); var hasValue = false; if (props != null && props.TryGetValue("_exp", out tmp)) { hasValue = true; if (tmp != null) { if (tmp is DateTime || tmp is DateTimeOffset) { expirationTime = (DateTime)tmp; } else { try { expirationTime = Convert.ToDateTime(tmp); } catch (Exception) { try { var num = Convert.ToInt64(tmp); expirationTime = Misc.OffsetFromEpoch(TimeSpan.FromSeconds(num)); } catch (Exception) { Log.To.Router.E(TAG, "Invalid value for _exp: {0}", tmp); return(StatusCode.BadRequest); } } } } props.Remove("_exp"); rev.SetProperties(props); } var castContext = context as ICouchbaseListenerContext2; var source = castContext != null && !castContext.IsLoopbackRequest ? castContext.Sender : null; StatusCode status = deleting ? StatusCode.Ok : StatusCode.Created; try { if (docId != null && docId.StartsWith("_local")) { if (expirationTime.HasValue) { return(StatusCode.BadRequest); } Log.To.Router.I(TAG, "Attempting to insert local {0} on top of {1} from PUT request", rev, prevRevId != null ? prevRevId.ToString() : "<root>"); outRev = db.Storage.PutLocalRevision(rev, prevRevId.AsRevID(), true); //TODO: Doesn't match iOS } else { Log.To.Router.I(TAG, "Attempting to insert {0} on top of {1} from PUT request", rev, prevRevId != null ? prevRevId.ToString() : "<root>"); outRev = db.PutRevision(rev, prevRevId.AsRevID(), allowConflict, source); if (hasValue) { db.Storage?.SetDocumentExpiration(rev.DocID, expirationTime); } } } catch (CouchbaseLiteException e) { status = e.Code; } return(status); }
public void OnCompletion(object response, Exception e) { try { Log.V(Log.TagSync, "%s: got /_revs_diff response"); IDictionary <string, object> results = (IDictionary <string, object>)response; if (e != null) { this._enclosing.SetError(e); this._enclosing.RevisionFailed(); } else { if (results.Count != 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: IList <object> docsToSend = new AList <object>(); RevisionList revsToSend = new RevisionList(); foreach (RevisionInternal rev in changes) { // Is this revision in the server's 'missing' list? IDictionary <string, object> properties = null; IDictionary <string, object> revResults = (IDictionary <string, object>)results.Get (rev.GetDocId()); if (revResults == null) { continue; } IList <string> revs = (IList <string>)revResults.Get("missing"); if (revs == null || !revs.Contains(rev.GetRevId())) { this._enclosing.RemovePending(rev); continue; } // Get the revision's properties: EnumSet <Database.TDContentOptions> contentOptions = EnumSet.Of(Database.TDContentOptions .TDIncludeAttachments); if (!this._enclosing.dontSendMultipart && this._enclosing.revisionBodyTransformationBlock == null) { contentOptions.AddItem(Database.TDContentOptions.TDBigAttachmentsFollow); } RevisionInternal loadedRev; try { loadedRev = this._enclosing.db.LoadRevisionBody(rev, contentOptions); properties = new Dictionary <string, object>(rev.GetProperties()); } catch (CouchbaseLiteException) { Log.W(Log.TagSync, "%s Couldn't get local contents of %s", rev, this._enclosing); this._enclosing.RevisionFailed(); continue; } RevisionInternal populatedRev = this._enclosing.TransformRevision(loadedRev); IList <string> possibleAncestors = (IList <string>)revResults.Get("possible_ancestors" ); properties = new Dictionary <string, object>(populatedRev.GetProperties()); IDictionary <string, object> revisions = this._enclosing.db.GetRevisionHistoryDictStartingFromAnyAncestor (populatedRev, possibleAncestors); properties.Put("_revisions", revisions); populatedRev.SetProperties(properties); // Strip any attachments already known to the target db: if (properties.ContainsKey("_attachments")) { // Look for the latest common ancestor and stub out older attachments: int minRevPos = Couchbase.Lite.Replicator.Pusher.FindCommonAncestor(populatedRev, possibleAncestors); Database.StubOutAttachmentsInRevBeforeRevPos(populatedRev, minRevPos + 1, false); properties = populatedRev.GetProperties(); if (!this._enclosing.dontSendMultipart && this._enclosing.UploadMultipartRevision (populatedRev)) { continue; } } if (properties == null || !properties.ContainsKey("_id")) { throw new InvalidOperationException("properties must contain a document _id"); } revsToSend.AddItem(rev); docsToSend.AddItem(properties); } //TODO: port this code from iOS // Post the revisions to the destination: this._enclosing.UploadBulkDocs(docsToSend, revsToSend); } else { // None of the revisions are new to the remote foreach (RevisionInternal revisionInternal in changes) { this._enclosing.RemovePending(revisionInternal); } } } } finally { Log.V(Log.TagSync, "%s | %s: processInbox.sendAsyncRequest() calling asyncTaskFinished()" , this, Sharpen.Thread.CurrentThread()); this._enclosing.AsyncTaskFinished(1); } }