/// <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); }