/// <summary>
        /// Finds the common ancestor.
        /// </summary>
        /// <remarks>
        /// Given a revision and an array of revIDs, finds the latest common ancestor revID
        /// and returns its generation #. If there is none, returns 0.
        /// </remarks>
        /// <returns>The common ancestor.</returns>
        /// <param name="rev">Rev.</param>
        /// <param name="possibleRevIDs">Possible rev I ds.</param>
        internal static int FindCommonAncestor(RevisionInternal rev, IList<string> possibleRevIDs)
        {
            if (possibleRevIDs == null || possibleRevIDs.Count == 0)
            {
                return 0;
            }

            var history = Database.ParseCouchDBRevisionHistory(rev.GetProperties());
            Debug.Assert(history != null);

            history = history.Intersect(possibleRevIDs).ToList();

            var ancestorID = history.Count == 0 
                ? null 
                : history[0];

            if (ancestorID == null)
            {
                return 0;
            }

            var parsed = RevisionInternal.ParseRevId(ancestorID);
            return parsed.Item1;
        }
        /// <summary>VALIDATION</summary>
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception>
        internal Status ValidateRevision(RevisionInternal newRev, RevisionInternal oldRev, String parentRevId)
        {
            var validations = Shared.GetValues("validation", Name);
            if (validations == null || validations.Count == 0) {
                return new Status(StatusCode.Ok);
            }

            var publicRev = new SavedRevision(this, newRev, parentRevId);
            var context = new ValidationContext(this, oldRev, newRev);
            Status status = new Status(StatusCode.Ok);
            foreach (var validationName in validations.Keys)
            {
                var validation = GetValidation(validationName);
                try {
                    validation(publicRev, context);
                } catch(Exception e) {
                    Log.E(TAG, String.Format("Validation block '{0}'", validationName), e);
                    status.Code = StatusCode.Exception;
                    break;
                }
                    
                if (context.RejectMessage != null) {
                    Log.D(TAG, "Failed update of {0}: {1}:{2} Old doc = {3}{2} New doc = {4}", oldRev, context.RejectMessage,
                            Environment.NewLine, oldRev == null ? null : oldRev.GetProperties(), newRev.GetProperties());
                    status.Code = StatusCode.Forbidden;
                    break;
                }
            }

            return status;
        }
        /// <summary>
        /// Queries the specified view using the specified options
        /// </summary>
        /// <returns>The HTTP response containing the results of the query</returns>
        /// <param name="context">The request context</param>
        /// <param name="db">The database to run the query in</param>
        /// <param name="view">The view to query</param>
        /// <param name="options">The options to apply to the query</param>
        public static CouchbaseLiteResponse QueryView(ICouchbaseListenerContext context, Database db, View view, QueryOptions options)
        {
            var result = view.QueryWithOptions(options);

            object updateSeq = options.UpdateSeq ? (object)view.LastSequenceIndexed : null;
            var mappedResult = new List<object>();
            foreach (var row in result) {
                row.Database = db;
                var dict = row.AsJSONDictionary();
                if (context.ContentOptions != DocumentContentOptions.None) {
                    var doc = dict.Get("doc").AsDictionary<string, object>();
                    if (doc != null) {
                        // Add content options:
                        RevisionInternal rev = new RevisionInternal(doc);
                        var status = new Status();
                        rev = DocumentMethods.ApplyOptions(context.ContentOptions, rev, context, db, status);
                        if (rev != null) {
                            dict["doc"] = rev.GetProperties();
                        }
                    }
                }

                mappedResult.Add(dict);
            }

            var body = new Body(new NonNullDictionary<string, object> {
                { "rows", mappedResult },
                { "total_rows", view.TotalRows },
                { "offset", options.Skip },
                { "update_seq", updateSeq }
            });

            var retVal = context.CreateResponse();
            retVal.JsonBody = body;
            return retVal;
        }
        internal bool ProcessAttachmentsForRevision(RevisionInternal rev, string prevRevId, Status status)
        {
            if (status == null) {
                status = new Status();
            }

            status.Code = StatusCode.Ok;
            var revAttachments = rev.GetAttachments();
            if (revAttachments == null) {
                return true; // no-op: no attachments
            }

            // Deletions can't have attachments:
            if (rev.IsDeleted() || revAttachments.Count == 0) {
                var body = rev.GetProperties();
                body.Remove("_attachments");
                rev.SetProperties(body);
                return true;
            }

            int generation = RevisionInternal.GenerationFromRevID(prevRevId) + 1;
            IDictionary<string, object> parentAttachments = null;
            return rev.MutateAttachments((name, attachInfo) =>
            {
                AttachmentInternal attachment = null;
                try {
                    attachment = new AttachmentInternal(name, attachInfo);
                } catch(CouchbaseLiteException e) {
                    return null;
                }

                if(attachment.EncodedContent != null) {
                    // If there's inline attachment data, decode and store it:
                    BlobKey blobKey = new BlobKey();
                    if(!Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey)) {
                        status.Code = StatusCode.AttachmentError;
                        return null;
                    }

                    attachment.BlobKey = blobKey;
                } else if(attachInfo.GetCast<bool>("follows")) {
                    // "follows" means the uploader provided the attachment in a separate MIME part.
                    // This means it's already been registered in _pendingAttachmentsByDigest;
                    // I just need to look it up by its "digest" property and install it into the store:
                    InstallAttachment(attachment, attachInfo);
                } else if(attachInfo.GetCast<bool>("stub")) {
                    // "stub" on an incoming revision means the attachment is the same as in the parent.
                    if(parentAttachments == null && prevRevId != null) {
                        parentAttachments = GetAttachmentsFromDoc(rev.GetDocId(), prevRevId, status);
                        if(parentAttachments == null) {
                            if(status.Code == StatusCode.Ok || status.Code == StatusCode.NotFound) {
                                status.Code = StatusCode.BadAttachment;
                            }

                            return null;
                        }
                    }

                    var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>();
                    if(parentAttachment == null) {
                        status.Code = StatusCode.BadAttachment;
                        return null;
                    }

                    return parentAttachment;
                }


                // Set or validate the revpos:
                if(attachment.RevPos == 0) {
                    attachment.RevPos = generation;
                } else if(attachment.RevPos >= generation) {
                    status.Code = StatusCode.BadAttachment;
                    return null;
                }

                Debug.Assert(attachment.IsValid);
                return attachment.AsStubDictionary();
            });
        }
        //Doesn't handle CouchbaseLiteException
        internal RevisionInternal LoadRevisionBody(RevisionInternal rev)
        {
            if (rev.GetSequence() > 0) {
                var props = rev.GetProperties();
                if (props != null && props.GetCast<string>("_rev") != null && props.GetCast<string>("_id") != null) {
                    return rev;
                }
            }

            Debug.Assert(rev.GetDocId() != null && rev.GetRevId() != null);
            Storage.LoadRevisionBody(rev);
            return rev;
        }
        public void TestStubOutAttachmentsInRevBeforeRevPos()
        {
            var hello = new JObject();
            hello["revpos"] = 1;
            hello["follows"] = true;

            var goodbye = new JObject();
            goodbye["revpos"] = 2;
            goodbye["data"] = "squeee";

            var attachments = new JObject();
            attachments["hello"] = hello;
            attachments["goodbye"] = goodbye;

            var properties = new Dictionary<string, object>();
            properties["_attachments"] = attachments;

            IDictionary<string, object> expected = null;

            var rev = new RevisionInternal(properties);
            Database.StubOutAttachmentsInRevBeforeRevPos(rev, 3, false);
            var checkAttachments = rev.GetProperties()["_attachments"].AsDictionary<string, object>();
            var result = (IDictionary<string, object>)checkAttachments["hello"];
            expected = new Dictionary<string, object>();
            expected["revpos"] = 1;
            expected["stub"] = true;
            AssertPropertiesAreEqual(expected, result);
            result = (IDictionary<string, object>)checkAttachments["goodbye"];
            expected = new Dictionary<string, object>();
            expected["revpos"] = 2;
            expected["stub"] = true;
            AssertPropertiesAreEqual(expected, result);

            rev = new RevisionInternal(properties);
            Database.StubOutAttachmentsInRevBeforeRevPos(rev, 2, false);
            checkAttachments = rev.GetProperties()["_attachments"].AsDictionary<string, object>();
            result = checkAttachments["hello"].AsDictionary<string, object>();
            expected = new Dictionary<string, object>();
            expected["revpos"] = 1;
            expected["stub"] = true;
            AssertPropertiesAreEqual(expected, result);
            result = checkAttachments["goodbye"].AsDictionary<string, object>();
            expected = goodbye.AsDictionary<string, object>();
            AssertPropertiesAreEqual(expected, result);

            rev = new RevisionInternal(properties);
            Database.StubOutAttachmentsInRevBeforeRevPos(rev, 1, false);
            checkAttachments = rev.GetProperties()["_attachments"].AsDictionary<string, object>();
            result = checkAttachments["hello"].AsDictionary<string, object>();
            expected = hello.AsDictionary<string, object>();
            AssertPropertiesAreEqual(expected, result);
            result = checkAttachments["goodbye"].AsDictionary<string, object>();
            expected = goodbye.AsDictionary<string, object>();
            AssertPropertiesAreEqual(expected, result);

            //Test the follows mode
            rev = new RevisionInternal(properties);
            Database.StubOutAttachmentsInRevBeforeRevPos(rev, 3, true);
            checkAttachments = rev.GetProperties()["_attachments"].AsDictionary<string, object>();
            result = checkAttachments["hello"].AsDictionary<string, object>();
            expected = new Dictionary<string, object>();
            expected["revpos"] = 1;
            expected["stub"] = true;
            AssertPropertiesAreEqual(expected, result);
            result = checkAttachments["goodbye"].AsDictionary<string, object>();
            expected = new Dictionary<string, object>();
            expected["revpos"] = 2;
            expected["stub"] = true;
            AssertPropertiesAreEqual(expected, result);

            rev = new RevisionInternal(properties);
            Database.StubOutAttachmentsInRevBeforeRevPos(rev, 2, true);
            checkAttachments = rev.GetProperties()["_attachments"].AsDictionary<string, object>();
            result = checkAttachments["hello"].AsDictionary<string, object>();
            expected = new Dictionary<string, object>();
            expected["revpos"] = 1;
            expected["stub"] = true;
            AssertPropertiesAreEqual(expected, result);
            result = checkAttachments["goodbye"].AsDictionary<string, object>();
            expected = new Dictionary<string, object>();
            expected["revpos"] = 2;
            expected["follows"] = true;
            AssertPropertiesAreEqual(expected, result);

            rev = new RevisionInternal(properties);
            Database.StubOutAttachmentsInRevBeforeRevPos(rev, 1, true);
            checkAttachments = rev.GetProperties()["_attachments"].AsDictionary<string, object>();
            result = checkAttachments["hello"].AsDictionary<string, object>();
            expected = new Dictionary<string, object>();
            expected["revpos"] = 1;
            expected["follows"] = true;
            AssertPropertiesAreEqual(expected, result);
            result = checkAttachments["goodbye"].AsDictionary<string, object>();
            expected = new Dictionary<string, object>();
            expected["revpos"] = 2;
            expected["follows"] = true;
            AssertPropertiesAreEqual(expected, result);
        }
 /// <summary>Stores a new (or initial) revision of a document.</summary>
 /// <remarks>
 /// 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.
 /// </remarks>
 /// <param name="oldRev">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>
 /// <param name="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>
 /// <param name="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>
 /// <param name="resultStatus">On return, an HTTP status code indicating success or failure.
 ///     </param>
 /// <returns>A new RevisionInternal with the docID, revID and sequence filled in (but no body).
 ///     </returns>
 /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception>
 internal RevisionInternal PutRevision(RevisionInternal oldRev, string prevRevId, bool allowConflict, Status resultStatus = null)
 {
     return PutDocument(oldRev.GetDocId(), oldRev.GetProperties(), prevRevId, allowConflict, resultStatus);
 }
 /// <summary>Stores a new (or initial) revision of a document.</summary>
 /// <remarks>
 /// 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.
 /// </remarks>
 /// <param name="oldRev">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>
 /// <param name="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>
 /// <param name="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>
 /// <returns>A new RevisionInternal with the docID, revID and sequence filled in (but no body).
 ///     </returns>
 /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception>
 internal RevisionInternal PutRevision(RevisionInternal oldRev, string prevRevId, bool allowConflict)
 {
     return PutDocument(oldRev.GetDocId(), oldRev.GetProperties(), prevRevId, allowConflict);
 }
        internal bool ProcessAttachmentsForRevision(RevisionInternal rev, IList<string> ancestry)
        {
            var revAttachments = rev.GetAttachments();
            if (revAttachments == null) {
                return true; // no-op: no attachments
            }

            // Deletions can't have attachments:
            if (rev.IsDeleted() || revAttachments.Count == 0) {
                var body = rev.GetProperties();
                body.Remove("_attachments");
                rev.SetProperties(body);
                return true;
            }

            var prevRevId = ancestry != null && ancestry.Count > 0 ? ancestry[0] : null;
            int generation = RevisionInternal.GenerationFromRevID(prevRevId) + 1;
            IDictionary<string, object> parentAttachments = null;
            return rev.MutateAttachments((name, attachInfo) =>
            {
                AttachmentInternal attachment = null;
                try {
                    attachment = new AttachmentInternal(name, attachInfo);
                } catch(CouchbaseLiteException) {
                    return null;
                }

                if(attachment.EncodedContent != null) {
                    // If there's inline attachment data, decode and store it:
                    BlobKey blobKey = new BlobKey();
                    if(!Attachments.StoreBlob(attachment.EncodedContent.ToArray(), blobKey)) {
                        throw new CouchbaseLiteException(
                            String.Format("Failed to write attachment ' {0}'to disk", name), StatusCode.AttachmentError);
                    }

                    attachment.BlobKey = blobKey;
                } else if(attachInfo.GetCast<bool>("follows")) {
                    // "follows" means the uploader provided the attachment in a separate MIME part.
                    // This means it's already been registered in _pendingAttachmentsByDigest;
                    // I just need to look it up by its "digest" property and install it into the store:
                    InstallAttachment(attachment);
                } else if(attachInfo.GetCast<bool>("stub")) {
                    // "stub" on an incoming revision means the attachment is the same as in the parent.
                    if(parentAttachments == null && prevRevId != null) {
                        parentAttachments = GetAttachmentsFromDoc(rev.GetDocId(), prevRevId);
                        if(parentAttachments == null) {
                            if(Attachments.HasBlobForKey(attachment.BlobKey)) {
                                // Parent revision's body isn't known (we are probably pulling a rev along
                                // with its entire history) but it's OK, we have the attachment already
                                return attachInfo;
                            }

                            var ancestorAttachment = FindAttachment(name, attachment.RevPos, rev.GetDocId(), ancestry);
                            if(ancestorAttachment != null) {
                                return ancestorAttachment;
                            }

                            throw new CouchbaseLiteException(
                                String.Format("Unable to find 'stub' attachment {0} in history", name), StatusCode.BadAttachment);
                        }
                    }

                    var parentAttachment = parentAttachments == null ? null : parentAttachments.Get(name).AsDictionary<string, object>();
                    if(parentAttachment == null) {
                        throw new CouchbaseLiteException(
                            String.Format("Unable to find 'stub' attachment {0} in history", name), StatusCode.BadAttachment);
                    }

                    return parentAttachment;
                }


                // Set or validate the revpos:
                if(attachment.RevPos == 0) {
                    attachment.RevPos = generation;
                } else if(attachment.RevPos > generation) {
                    throw new CouchbaseLiteException(
                        String.Format("Attachment specifies revision generation {0} but document is only at revision generation {1}",
                        attachment.RevPos, generation), StatusCode.BadAttachment);
                }

                Debug.Assert(attachment.IsValid);
                return attachment.AsStubDictionary();
            });
        }
       private void VerifyRev(RevisionInternal rev, IList<RevisionID> history)
        {
            var gotRev = database.GetDocument(rev.DocID, null, true);
            Assert.AreEqual(rev, gotRev);
            Assert.AreEqual(rev.GetProperties(), gotRev.GetProperties());

            var revHistory = database.GetRevisionHistory(gotRev, null);
            Assert.AreEqual(history.Count, revHistory.Count);
            for(var i = 0; i < history.Count; i++) {
                Assert.AreEqual(history[i], revHistory[i]);
            }
        }
        internal RevisionInternal PutDocument(string docId, IDictionary<string, object> properties, string prevRevId, bool allowConflict)
        {
            bool deleting = properties == null || properties.GetCast<bool>("_deleted");
            Log.D(TAG, "PUT _id={0}, _rev={1}, _deleted={2}, allowConflict={3}", docId, prevRevId, deleting, allowConflict);
            if ((prevRevId != null && docId == null) || (deleting && docId == null)) {
                throw new CouchbaseLiteException(StatusCode.BadId);
            }

            if (properties != null && properties.Get("_attachments").AsDictionary<string, object>() != null) {
                var tmpRev = new RevisionInternal(docId, prevRevId, deleting);
                tmpRev.SetProperties(properties);
                if (!ProcessAttachmentsForRevision(tmpRev, prevRevId == null ? null : new List<string> { prevRevId })) {
                    return null;
                }

                properties = tmpRev.GetProperties();
            }

            StoreValidation validationBlock = null;
            if (Shared.HasValues("validation", Name)) {
                validationBlock = ValidateRevision;
            }

            var putRev = Storage.PutRevision(docId, prevRevId, properties, deleting, allowConflict, validationBlock);
            if (putRev != null) {
                Log.D(TAG, "--> created {0}", putRev);
                if (!string.IsNullOrEmpty(docId)) {
                    var dummy = default(WeakReference);
                    UnsavedRevisionDocumentCache.TryRemove(docId, out dummy);
                }
            }

            return putRev;
        }
        /// <summary>
        /// Uploads the revision as JSON instead of multipart.
        /// </summary>
        /// <remarks>
        /// Fallback to upload a revision if UploadMultipartRevision failed due to the server's rejecting
        /// multipart format.
        /// </remarks>
        /// <param name="rev">Rev.</param>
        private void UploadJsonRevision(RevisionInternal rev)
        {
            // Get the revision's properties:
            if (!LocalDatabase.InlineFollowingAttachmentsIn(rev))
            {
                LastError = new CouchbaseLiteException(StatusCode.BadAttachment);
                RevisionFailed();
                return;
            }

            var path = string.Format("/{0}?new_edits=false", Uri.EscapeUriString(rev.GetDocId()));
            SendAsyncRequest(HttpMethod.Put, path, rev.GetProperties(), (result, e) =>
            {
                if (e != null) 
                {
                    LastError = e;
                    RevisionFailed();
                } 
                else 
                {
                    Log.V(TAG, "Sent {0} (JSON), response={1}", rev, result);
                    SafeIncrementCompletedChangesCount();
                    RemovePending (rev);
                }
            });
        }
        private bool UploadMultipartRevision(RevisionInternal revision)
        {
            MultipartContent multiPart = null;
            var revProps = revision.GetProperties();

            var attachments = revProps.Get("_attachments").AsDictionary<string,object>();
            foreach (var attachmentKey in attachments.Keys) {
                var attachment = attachments.Get(attachmentKey).AsDictionary<string,object>();
                if (attachment.ContainsKey("follows")) {
                    if (multiPart == null) {
                        multiPart = new MultipartContent("related");
                        try {
                            var json = Manager.GetObjectMapper().WriteValueAsString(revProps);
                            var utf8charset = Encoding.UTF8;
                            //multiPart.Add(new StringContent(json, utf8charset, "application/json"), "param1");

                            var jsonContent = new StringContent(json, utf8charset, "application/json");
                            //jsonContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
                            multiPart.Add(jsonContent);
                        } catch (IOException e) {
                            throw new ArgumentException("Not able to serialize revision properties into a multipart request content.", e);
                        }
                    }

                    var blobStore = LocalDatabase.Attachments;
                    var base64Digest = (string)attachment.Get("digest");

                    var blobKey = new BlobKey(base64Digest);
                    var inputStream = blobStore.BlobStreamForKey(blobKey);

                    if (inputStream == null) {
                        Log.W(TAG, "Unable to find blob file for blobKey: " + blobKey + " - Skipping upload of multipart revision.");
                        multiPart = null;
                    } else {
                        string contentType = null;
                        if (attachment.ContainsKey("content_type")) {
                            contentType = (string)attachment.Get("content_type");
                        } else {
                            if (attachment.ContainsKey("content-type")) {
                                var message = string.Format("Found attachment that uses content-type"
                                              + " field name instead of content_type (see couchbase-lite-android"
                                              + " issue #80): " + attachment);
                                Log.W(TAG, message);
                            }
                        }

                        var content = new StreamContent(inputStream);
                        content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") {
                            FileName = attachmentKey
                        };
                        content.Headers.ContentType = new MediaTypeHeaderValue(contentType ?? "application/octet-stream");

                        multiPart.Add(content);
                    }
                }
            }

            if (multiPart == null) {
                return false;
            }

            var path = string.Format("/{0}?new_edits=false", revision.GetDocId());

            // TODO: need to throttle these requests
            Log.D(TAG, "Uploading multipart request.  Revision: " + revision);
            SafeAddToChangesCount(1);
            SendAsyncMultipartRequest(HttpMethod.Put, path, multiPart, (result, e) => 
            {
                if (e != null) {
                    var httpError = e as HttpResponseException;
                    if (httpError != null) {
                        if (httpError.StatusCode == System.Net.HttpStatusCode.UnsupportedMediaType) {
                            _dontSendMultipart = true;
                            UploadJsonRevision(revision);
                        }
                    } else {
                        Log.E (TAG, "Exception uploading multipart request", e);
                        LastError = e;
                        RevisionFailed();
                    }
                } else {
                    Log.D (TAG, "Uploaded multipart request.  Result: " + result);
                    SafeIncrementCompletedChangesCount();
                    RemovePending(revision);
                }
            });

            return true;
        }
        private MultipartWriter GetMultipartWriter(RevisionInternal rev, string boundary)
        {
            // Find all the attachments with "follows" instead of a body, and put 'em in a multipart stream.
            // It's important to scan the _attachments entries in the same order in which they will appear
            // in the JSON, because CouchDB expects the MIME bodies to appear in that same order
            var bodyStream = default(MultipartWriter);
            var attachments = rev.GetAttachments();
            foreach (var a in attachments) {
                var attachment = a.Value.AsDictionary<string, object>();
                if (attachment != null && attachment.GetCast<bool>("follows")) {
                    if (bodyStream == null) {
                        // Create the HTTP multipart stream:
                        bodyStream = new MultipartWriter("multipart/related", boundary);
                        bodyStream.SetNextPartHeaders(new Dictionary<string, string> { 
                            { "Content-Type", "application/json" } 
                        });

                        // Use canonical JSON encoder so that _attachments keys will be written in the
                        // same order that this for loop is processing the attachments.
                        var json = Manager.GetObjectMapper().WriteValueAsBytes(rev.GetProperties(), true);
                        if (CanSendCompressedRequests) {
                            bodyStream.AddGZippedData(json);
                        } else {
                            bodyStream.AddData(json);
                        }
                    }

                    // Add attachment as another MIME part:
                    var disposition = String.Format("attachment; filename={0}", Misc.QuoteString(a.Key));
                    var contentType = attachment.GetCast<string>("type");
                    var contentEncoding = attachment.GetCast<string>("encoding");
                    bodyStream.SetNextPartHeaders(new NonNullDictionary<string, string> {
                        { "Content-Disposition", disposition },
                        { "Content-Type", contentType },
                        { "Content-Encoding", contentEncoding }
                    });

                    var attachmentObj = default(AttachmentInternal);
                    try {
                        attachmentObj = LocalDatabase.AttachmentForDict(attachment, a.Key);
                    } catch(CouchbaseLiteException) {
                        return null;
                    }

                    bodyStream.AddStream(attachmentObj.ContentStream, attachmentObj.Length);
                }
            }

            return bodyStream;
        }
        /// <summary>INSERTION:</summary>
        internal IEnumerable<Byte> EncodeDocumentJSON(RevisionInternal rev)
        {
            var origProps = rev.GetProperties();
            if (origProps == null)
            {
                return null;
            }
            var specialKeysToLeave = new[] { "_removed", "_replication_id", "_replication_state", "_replication_state_time" };

            // Don't allow any "_"-prefixed keys. Known ones we'll ignore, unknown ones are an error.
            var properties = new Dictionary<String, Object>(origProps.Count);
            foreach (var key in origProps.Keys)
            {
                var shouldAdd = false;
                if (key.StartsWith("_", StringComparison.InvariantCultureIgnoreCase))
                {
                    if (!KnownSpecialKeys.Contains(key))
                    {
                        Log.E(Tag, "Database: Invalid top-level key '" + key + "' in document to be inserted");
                        return null;
                    }
                    if (specialKeysToLeave.Contains(key))
                    {
                        shouldAdd = true;
                    }
                }
                else
                {
                    shouldAdd = true;
                }
                if (shouldAdd)
                {
                    properties.Put(key, origProps.Get(key));
                }
            }
            IEnumerable<byte> json = null;
            try
            {
                json = Manager.GetObjectMapper().WriteValueAsBytes(properties);
            }
            catch (Exception e)
            {
                Log.E(Tag, "Error serializing " + rev + " to JSON", e);
            }
            return json;
        }
        //Doesn't handle CouchbaseLiteException
        internal RevisionInternal LoadRevisionBody(RevisionInternal rev)
        {
            if (!IsOpen) {
                Log.W(TAG, "LoadRevisionBody called on closed database");
                return null;
            }

            if (rev.GetSequence() > 0) {
                var props = rev.GetProperties();
                if (props != null && props.GetCast<string>("_rev") != null && props.GetCast<string>("_id") != null) {
                    return rev;
                }
            }



            Debug.Assert(rev.GetDocId() != null && rev.GetRevId() != null);
            Storage.LoadRevisionBody(rev);
            return rev;
        }
        /// <summary>Updates or deletes an attachment, creating a new document revision in the process.
        ///     </summary>
        /// <remarks>
        /// Updates or deletes an attachment, creating a new document revision in the process.
        /// Used by the PUT / DELETE methods called on attachment URLs.
        /// </remarks>
        /// <exclude></exclude>
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception>
        internal RevisionInternal UpdateAttachment(string filename, BlobStoreWriter body, string contentType, AttachmentEncoding encoding, string docID, string oldRevID)
        {
            var isSuccessful = false;
            if (String.IsNullOrEmpty (filename) || (body != null && contentType == null) || (oldRevID != null && docID == null) || (body != null && docID == null))
            {
                throw new CouchbaseLiteException(StatusCode.BadRequest);
            }

            BeginTransaction();

            try
            {
                var oldRev = new RevisionInternal(docID, oldRevID, false, this);
                if (oldRevID != null)
                {
                    // Load existing revision if this is a replacement:
                    try
                    {
                        LoadRevisionBody(oldRev, DocumentContentOptions.None);
                    }
                    catch (CouchbaseLiteException e)
                    {
                        if (e.GetCBLStatus().GetCode() == StatusCode.NotFound && ExistsDocumentWithIDAndRev(docID, null))
                        {
                            throw new CouchbaseLiteException(StatusCode.Conflict);
                        }
                    }
                }
                else
                {
                    // If this creates a new doc, it needs a body:
                    oldRev.SetBody(new Body(new Dictionary<string, object>()));
                }
                // Update the _attachments dictionary:
                var oldRevProps = oldRev.GetProperties();
                IDictionary<string, object> attachments = null;
                if (oldRevProps != null)
                {
                    attachments = (IDictionary<string, object>)oldRevProps.Get("_attachments");
                }
                if (attachments == null)
                {
                    attachments = new Dictionary<string, object>();
                }
                if (body != null)
                {
                    var key = body.GetBlobKey();
                    var digest = key.Base64Digest();

                    var blobsByDigest = new Dictionary<string, BlobStoreWriter>();
                    blobsByDigest.Put(digest, body);

                    RememberAttachmentWritersForDigests(blobsByDigest);

                    var encodingName = (encoding == AttachmentEncoding.AttachmentEncodingGZIP) ? "gzip" : null;
                    var dict = new Dictionary<string, object>();
                    dict.Put("digest", digest);
                    dict.Put("length", body.GetLength());
                    dict.Put("follows", true);
                    dict.Put("content_type", contentType);
                    dict.Put("encoding", encodingName);

                    attachments.Put(filename, dict);
                }
                else
                {
                    if (oldRevID != null && !attachments.ContainsKey(filename))
                    {
                        throw new CouchbaseLiteException(StatusCode.NotFound);
                    }
                    attachments.Remove(filename);
                }

                var properties = oldRev.GetProperties();
                properties.Put("_attachments", attachments);
                oldRev.SetProperties(properties);

                // Create a new revision:
                var putStatus = new Status();
                var newRev = PutRevision(oldRev, oldRevID, false, putStatus);

                isSuccessful = true;

                return newRev;
            }
            catch (SQLException e)
            {
                Log.E(Tag, "Error updating attachment", e);
                throw new CouchbaseLiteException(StatusCode.InternalServerError);
            }
            finally
            {
                EndTransaction(isSuccessful);
            }
        }
        // Apply the options in the URL query to the specified revision and create a new revision object
        internal static RevisionInternal ApplyOptions(DocumentContentOptions options, RevisionInternal rev, ICouchbaseListenerContext context,
            Database db, Status outStatus)
        {
            if ((options & (DocumentContentOptions.IncludeRevs | DocumentContentOptions.IncludeRevsInfo | DocumentContentOptions.IncludeConflicts |
                DocumentContentOptions.IncludeAttachments | DocumentContentOptions.IncludeLocalSeq)) != 0) {
                var dst = rev.GetProperties(); 
                if (options.HasFlag(DocumentContentOptions.IncludeLocalSeq)) {
                    dst["_local_seq"] = rev.GetSequence();
                }

                if (options.HasFlag(DocumentContentOptions.IncludeRevs)) {
                    var revs = db.GetRevisionHistory(rev, null);
                    dst["_revisions"] = Database.MakeRevisionHistoryDict(revs);
                }

                if (options.HasFlag(DocumentContentOptions.IncludeRevsInfo)) {
                    dst["_revs_info"] = db.Storage.GetRevisionHistory(rev, null).Select(x =>
                    {
                        string status = "available";
                        if(x.IsDeleted()) {
                            status = "deleted";
                        } else if(x.IsMissing()) {
                            status = "missing";
                        }

                        return new Dictionary<string, object> {
                            { "rev", x.GetRevId() },
                            { "status", status }
                        };
                    });
                }

                if (options.HasFlag(DocumentContentOptions.IncludeConflicts)) {
                    RevisionList revs = db.Storage.GetAllDocumentRevisions(rev.GetDocId(), true);
                    if (revs.Count > 1) {
                        dst["_conflicts"] = revs.Select(x =>
                        {
                            return x.Equals(rev) || x.IsDeleted() ? null : x.GetRevId();
                        });
                    }
                }

                RevisionInternal nuRev = new RevisionInternal(dst);
                if (options.HasFlag(DocumentContentOptions.IncludeAttachments)) {
                    bool attEncodingInfo = context != null && context.GetQueryParam<bool>("att_encoding_info", bool.TryParse, false);
                    if(!db.ExpandAttachments(nuRev, 0, false, !attEncodingInfo, outStatus)) {
                        return null;
                    }
                }

                rev = nuRev;
            }

            return rev;
        }
        internal RevisionInternal PutDocument(string docId, IDictionary<string, object> properties, string prevRevId, bool allowConflict, Status resultStatus)
        {
            bool deleting = properties == null || properties.GetCast<bool>("_deleted");
            Log.D(TAG, "PUT _id={0}, _rev={1}, _deleted={2}, allowConflict={3}", docId, prevRevId, deleting, allowConflict);
            if ((prevRevId != null && docId == null) || (deleting && docId == null)) {
                if (resultStatus != null) {
                    resultStatus.Code = StatusCode.BadId;
                    return null;
                }
            }

            if (properties != null && properties.Get("_attachments").AsDictionary<string, object>() != null) {
                var tmpRev = new RevisionInternal(docId, prevRevId, deleting);
                tmpRev.SetProperties(properties);
                if (!ProcessAttachmentsForRevision(tmpRev, prevRevId, resultStatus)) {
                    return null;
                }

                properties = tmpRev.GetProperties();
            }

            StoreValidation validationBlock = null;
            if (Shared.HasValues("validation", Name)) {
                validationBlock = ValidateRevision;
            }

            var putRev = Storage.PutRevision(docId, prevRevId, properties, deleting, allowConflict, validationBlock, resultStatus);
            if (putRev != null) {
                Log.D(TAG, "--> created {0}", putRev);
                if (!string.IsNullOrEmpty(docId)) {
                    UnsavedRevisionDocumentCache.Remove(docId);
                }
            }

            return putRev;
        }
        public void ForceInsert(RevisionInternal inRev, IList<string> revHistory, StoreValidation validationBlock, Uri source)
        {
            if (_config.HasFlag(C4DatabaseFlags.ReadOnly)) {
                throw new CouchbaseLiteException("Attempting to write to a readonly database", StatusCode.Forbidden);
            }

            var json = Manager.GetObjectMapper().WriteValueAsString(inRev.GetProperties(), true);
            var change = default(DocumentChange);
            RunInTransaction(() =>
            {
                // First get the CBForest doc:
                WithC4Document(inRev.GetDocId(), null, false, true, doc =>
                {
                    ForestDBBridge.Check(err => Native.c4doc_insertRevisionWithHistory(doc, json, inRev.IsDeleted(), 
                        inRev.GetAttachments() != null, revHistory.ToArray(), err));

                    // Save updated doc back to the database:
                    var isWinner = SaveDocument(doc, revHistory[0], inRev.GetProperties());
                    inRev.SetSequence((long)doc->sequence);
                    change = ChangeWithNewRevision(inRev, isWinner, doc, source);
                });

                return true;
            });

            if (change != null && Delegate != null) {
                Delegate.DatabaseStorageChanged(change);
            }
        }
        /// <summary>
        /// Given a newly-added revision, adds the necessary attachment rows to the sqliteDb and
        /// stores inline attachments into the blob store.
        /// </summary>
        /// <remarks>
        /// Given a newly-added revision, adds the necessary attachment rows to the sqliteDb and
        /// stores inline attachments into the blob store.
        /// </remarks>
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception>
        internal void ProcessAttachmentsForRevision(IDictionary<string, AttachmentInternal> attachments, RevisionInternal rev, long parentSequence)
        {
            Debug.Assert((rev != null));
            var newSequence = rev.GetSequence();
            Debug.Assert((newSequence > parentSequence));
            var generation = rev.GetGeneration();
            Debug.Assert((generation > 0));

            // If there are no attachments in the new rev, there's nothing to do:
            IDictionary<string, object> revAttachments = null;
            var properties = rev.GetProperties ();
            if (properties != null) {
                revAttachments = properties.Get("_attachments").AsDictionary<string, object>();
            }

            if (revAttachments == null || revAttachments.Count == 0 || rev.IsDeleted()) {
                return;
            }

            foreach (string name in revAttachments.Keys)
            {
                var attachment = attachments.Get(name);
                if (attachment != null)
                {
                    // 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.
                    if (attachment.RevPos == 0)
                    {
                        attachment.RevPos = generation;
                    }
                    else
                    {
                        if (attachment.RevPos > generation)
                        {
                            Log.W(TAG, string.Format("Attachment {0} {1} has unexpected revpos {2}, setting to {3}", rev, name, attachment.RevPos, generation));
                            attachment.RevPos = generation;
                        }
                    }
                }
            }
        }
        public RevisionInternal PutLocalRevision(RevisionInternal revision, string prevRevId, bool obeyMVCC)
        {
            var docId = revision.GetDocId();
            if (!docId.StartsWith("_local/")) {
                throw new CouchbaseLiteException("Local revision IDs must start with _local/", StatusCode.BadId);
            }

            if (revision.IsDeleted()) {
                DeleteLocalRevision(docId, prevRevId, obeyMVCC);
                return revision;
            }

            var result = default(RevisionInternal);
            RunInTransaction(() =>
            {
                var json = Manager.GetObjectMapper().WriteValueAsString(revision.GetProperties(), true);
                WithC4Raw(docId, "_local", doc => 
                {
                    var generation = RevisionInternal.GenerationFromRevID(prevRevId);
                    if(obeyMVCC) {
                        if(prevRevId != null) {
                            if(prevRevId != (doc != null ? (string)doc->meta : null)) {
                                throw new CouchbaseLiteException(StatusCode.Conflict);
                            }

                            if(generation == 0) {
                                throw new CouchbaseLiteException(StatusCode.BadId);
                            }
                        } else if(doc != null) {
                            throw new CouchbaseLiteException(StatusCode.Conflict);
                        }
                    }

                    var newRevId = String.Format("{0}-local", ++generation);
                    ForestDBBridge.Check(err => Native.c4raw_put(Forest, "_local", docId, newRevId, json, err));
                    result = revision.CopyWithDocID(docId, newRevId);
                });

                return true;
            });

            return result;
        }
        internal RevisionInternal RevisionByLoadingBody(RevisionInternal rev, Status outStatus)
        {
            // First check for no-op -- if we just need the default properties and already have them:
            if (rev.GetSequence() != 0) {
                var props = rev.GetProperties();
                if (props != null && props.ContainsKey("_rev") && props.ContainsKey("_id")) {
                    if (outStatus != null) {
                        outStatus.Code = StatusCode.Ok;
                    }

                    return rev;
                }
            }

            RevisionInternal nuRev = rev.CopyWithDocID(rev.GetDocId(), rev.GetRevId());
            try {
                LoadRevisionBody(nuRev);
            } catch(CouchbaseLiteException e) {
                if (outStatus != null) {
                    outStatus.Code = e.CBLStatus.Code;
                }

                nuRev = null;
            }

            return nuRev;
        }
        private void VerifyHistory(Database db, RevisionInternal rev, IList<string> history)
        {
            var gotRev = db.GetDocument(rev.GetDocId(), null, 
                true);
            Assert.AreEqual(rev, gotRev);
            AssertPropertiesAreEqual(rev.GetProperties(), gotRev.GetProperties());

            var revHistory = db.Storage.GetRevisionHistory(gotRev, null);
            Assert.AreEqual(history.Count, revHistory.Count);
            
            for (int i = 0; i < history.Count; i++)
            {
                RevisionInternal hrev = revHistory[i];
                Assert.AreEqual(rev.GetDocId(), hrev.GetDocId());
                Assert.AreEqual(history[i], hrev.GetRevId());
                Assert.IsFalse(rev.IsDeleted());
            }
        }
        /// <summary>Updates or deletes an attachment, creating a new document revision in the process.
        ///     </summary>
        /// <remarks>
        /// Updates or deletes an attachment, creating a new document revision in the process.
        /// Used by the PUT / DELETE methods called on attachment URLs.
        /// </remarks>
        /// <exclude></exclude>
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception>
        internal RevisionInternal UpdateAttachment(string filename, BlobStoreWriter body, string contentType, AttachmentEncoding encoding, string docID, string oldRevID)
        {
            if(StringEx.IsNullOrWhiteSpace(filename) || (body != null && contentType == null) || 
                (oldRevID != null && docID == null) || (body != null && docID == null)) {
                throw new CouchbaseLiteException(StatusCode.BadAttachment);
            }

            var oldRev = new RevisionInternal(docID, oldRevID, false);
            if (oldRevID != null) {
                // Load existing revision if this is a replacement:
                try {
                    oldRev = LoadRevisionBody(oldRev);
                } catch (CouchbaseLiteException e) {
                    if (e.Code == StatusCode.NotFound && GetDocument(docID, null, false) != null) {
                        throw new CouchbaseLiteException(StatusCode.Conflict);
                    }

                    throw;
                }
            } else {
                // If this creates a new doc, it needs a body:
                oldRev.SetBody(new Body(new Dictionary<string, object>()));
            }

            // Update the _attachments dictionary:
            var attachments = oldRev.GetProperties().Get("_attachments").AsDictionary<string, object>();
            if (attachments == null) {
                attachments = new Dictionary<string, object>();
            }

            if (body != null) {
                var key = body.GetBlobKey();
                string digest = key.Base64Digest();
                RememberAttachmentWriter(body);
                string encodingName = (encoding == AttachmentEncoding.GZIP) ? "gzip" : null;
                attachments[filename] = new NonNullDictionary<string, object> {
                    { "digest", digest },
                    { "length", body.GetLength() },
                    { "follows", true },
                    { "content_type", contentType },
                    { "encoding", encodingName }
                };
            } else {
                if (oldRevID != null && attachments.Get(filename) == null) {
                    throw new CouchbaseLiteException(StatusCode.AttachmentNotFound);
                }

                attachments.Remove(filename);
            }

            var properties = oldRev.GetProperties();
            properties["_attachments"] = attachments;
            oldRev.SetProperties(properties);

            Status status = new Status();
            var newRev = PutRevision(oldRev, oldRevID, false, status);
            if (status.IsError) {
                throw new CouchbaseLiteException(status.Code);
            }

            return newRev;
        }
        /// <summary>
        /// Given a newly-added revision, adds the necessary attachment rows to the sqliteDb and
        /// stores inline attachments into the blob store.
        /// </summary>
        /// <remarks>
        /// Given a newly-added revision, adds the necessary attachment rows to the sqliteDb and
        /// stores inline attachments into the blob store.
        /// </remarks>
        /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception>
        internal void ProcessAttachmentsForRevision(IDictionary<string, AttachmentInternal> attachments, RevisionInternal rev, long parentSequence)
        {
            Debug.Assert((rev != null));
            var newSequence = rev.GetSequence();
            Debug.Assert((newSequence > parentSequence));
            var generation = rev.GetGeneration();
            Debug.Assert((generation > 0));

            // If there are no attachments in the new rev, there's nothing to do:
            IDictionary<string, object> revAttachments = null;
            var properties = rev.GetProperties ();
            if (properties != null)
            {
                revAttachments = properties.Get("_attachments").AsDictionary<string, object>();
            }

            if (revAttachments == null || revAttachments.Count == 0 || rev.IsDeleted())
            {
                return;
            }

            foreach (string name in revAttachments.Keys)
            {
                var attachment = attachments.Get(name);
                if (attachment != null)
                {
                    // 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.
                    if (attachment.GetRevpos() == 0)
                    {
                        attachment.SetRevpos(generation);
                    }
                    else
                    {
                        if (attachment.GetRevpos() > generation)
                        {
                            Log.W(Tag, string.Format("Attachment {0} {1} has unexpected revpos {2}, setting to {3}", rev, name, attachment.GetRevpos(), generation));
                            attachment.SetRevpos(generation);
                        }
                    }
                    // Finally insert the attachment:
                    InsertAttachmentForSequence(attachment, newSequence);
                }
                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?
                    CopyAttachmentNamedFromSequenceToSequence(name, parentSequence, newSequence);
                }
            }
        }
 /// <summary>
 /// Creates a dictionary of metadata for one specific revision
 /// </summary>
 /// <returns>The metadata dictionary</returns>
 /// <param name="rev">The revision to examine</param>
 /// <param name="responseState">The current response state</param>
 public static IDictionary<string, object> ChangesDictForRev(RevisionInternal rev, DBMonitorCouchbaseResponseState responseState)
 {
     if (responseState.ChangesIncludeDocs) {
         var status = new Status();
         var rev2 = DocumentMethods.ApplyOptions(responseState.ContentOptions, rev, responseState.Context, responseState.Db, status);
         if (rev2 != null) {
             rev2.Sequence = rev.Sequence;
             rev = rev2;
         }
     }
     return new NonNullDictionary<string, object> {
         { "seq", rev.Sequence },
         { "id", rev.DocID },
         { "changes", new List<object> { 
                 new Dictionary<string, object> { 
                     { "rev", rev.RevID } 
                 } 
             } 
         },
         { "deleted", rev.Deleted ? (object)true : null },
         { "doc", responseState.ChangesIncludeDocs ? rev.GetProperties() : null }
     };
 }
        internal void StubOutAttachmentsInRevision(IDictionary<String, AttachmentInternal> attachments, RevisionInternal rev)
        {
            var properties = rev.GetProperties();
            var attachmentProps = properties.Get("_attachments");
            if (attachmentProps != null)
            {
                var nuAttachments = new Dictionary<string, object>();
                foreach (var kvp in attachmentProps.AsDictionary<string,object>())
                {
                    var attachmentValue = kvp.Value.AsDictionary<string,object>();
                    if (attachmentValue.ContainsKey("follows") || attachmentValue.ContainsKey("data"))
                    {
                        attachmentValue.Remove("follows");
                        attachmentValue.Remove("data");

                        attachmentValue["stub"] = true;
                        if (attachmentValue.Get("revpos") == null)
                        {
                            attachmentValue.Put("revpos", rev.GetGeneration());
                        }

                        var attachmentObject = attachments.Get(kvp.Key);
                        if (attachmentObject != null)
                        {
                            attachmentValue.Put("length", attachmentObject.GetLength());
                            if (attachmentObject.GetBlobKey() != null)
                            {
                                attachmentValue.Put("digest", attachmentObject.GetBlobKey().Base64Digest());
                            }
                        }
                    }
                    nuAttachments[kvp.Key] = attachmentValue;
                }

                properties["_attachments"] = nuAttachments;  
            }
        }
        internal RevisionInternal TransformRevision(RevisionInternal rev)
        {
            if (RevisionBodyTransformationFunction != null) {
                try {
                    var generation = rev.GetGeneration();
                    var xformed = RevisionBodyTransformationFunction(rev);
                    if (xformed == null) {
                        return null;
                    }

                    if (xformed != rev) {
                        Debug.Assert((xformed.GetDocId().Equals(rev.GetDocId())));
                        Debug.Assert((xformed.GetRevId().Equals(rev.GetRevId())));
                        Debug.Assert((xformed.GetProperties().Get("_revisions").Equals(rev.GetProperties().Get("_revisions"))));

                        if (xformed.GetProperties().ContainsKey("_attachments")) {
                            // Insert 'revpos' properties into any attachments added by the callback:
                            var mx = new RevisionInternal(xformed.GetProperties());
                            xformed = mx;
                            mx.MutateAttachments((name, info) =>
                            {
                                if (info.Get("revpos") != null) {
                                    return info;
                                }

                                if (info.Get("data") == null) {
                                    throw new InvalidOperationException("Transformer added attachment without adding data");
                                }

                                var newInfo = new Dictionary<string, object>(info);
                                newInfo["revpos"] = generation;
                                return newInfo;
                            });
                        }
                    }
                } catch (Exception e) {
                    Log.W(TAG, String.Format("Exception transforming a revision of doc '{0}'", rev.GetDocId()), e);
                }
            }

            return rev;
        }
        public void TestLoadRevisionBody()
        {
            var document = database.CreateDocument();
            var properties = new Dictionary<string, object>();
            properties["foo"] = "foo";
            properties["bar"] = false;
            properties["_id"] = document.Id;
            document.PutProperties(properties);
            properties.SetRevID(document.CurrentRevisionId);
            Assert.IsNotNull(document.CurrentRevision);

            var revisionInternal = new RevisionInternal(
                document.Id, document.CurrentRevisionId.AsRevID(), false);

            database.LoadRevisionBody(revisionInternal);
            Assert.AreEqual(properties, revisionInternal.GetProperties());
            revisionInternal.SetBody(null);

            // now lets purge the document, and then try to load the revision body again
            document.Purge();

            var gotExpectedException = false;
            try {
                database.LoadRevisionBody(revisionInternal);
            } catch (CouchbaseLiteException e) {
                gotExpectedException |= 
                    e.CBLStatus.Code == StatusCode.NotFound;
            }

            Assert.IsTrue(gotExpectedException);
        }