/// <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, Boolean allowConflict, Status resultStatus) { // prevRevId is the rev ID being replaced, or nil if an insert var docId = oldRev.GetDocId(); var deleted = oldRev.IsDeleted(); if ((oldRev == null) || ((prevRevId != null) && (docId == null)) || (deleted && (docId == null)) || ((docId != null) && !IsValidDocumentId(docId))) { throw new CouchbaseLiteException(StatusCode.BadRequest); } BeginTransaction(); Cursor cursor = null; var inConflict = false; RevisionInternal winningRev = null; RevisionInternal newRev = null; // PART I: In which are performed lookups and validations prior to the insert... var docNumericID = (docId != null) ? GetDocNumericID(docId) : 0; var parentSequence = 0L; string oldWinningRevID = null; try { var oldWinnerWasDeletion = false; var wasConflicted = false; if (docNumericID > 0) { var outIsDeleted = new AList<bool>(); var outIsConflict = new AList<bool>(); try { oldWinningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict); oldWinnerWasDeletion |= outIsDeleted.Count > 0; wasConflicted |= outIsConflict.Count > 0; } catch (Exception e) { Sharpen.Runtime.PrintStackTrace(e); } } if (prevRevId != null) { // Replacing: make sure given prevRevID is current & find its sequence number: if (docNumericID <= 0) { var msg = string.Format("No existing revision found with doc id: {0}", docId); throw new CouchbaseLiteException(msg, StatusCode.NotFound); } parentSequence = GetSequenceOfDocument(docNumericID, prevRevId, !allowConflict); if (parentSequence == 0) { // Not found: either a 404 or a 409, depending on whether there is any current revision if (!allowConflict && ExistsDocumentWithIDAndRev(docId, null)) { var msg = string.Format("Conflicts not allowed and there is already an existing doc with id: {0}", docId); throw new CouchbaseLiteException(msg, StatusCode.Conflict); } else { var msg = string.Format("No existing revision found with doc id: {0}", docId); throw new CouchbaseLiteException(msg, StatusCode.NotFound); } } if (_validations != null && _validations.Count > 0) { // Fetch the previous revision and validate the new one against it: var oldRevCopy = oldRev.CopyWithDocID(oldRev.GetDocId(), null); var prevRev = new RevisionInternal(docId, prevRevId, false, this); ValidateRevision(oldRevCopy, prevRev, prevRevId); } } else { // Inserting first revision. if (deleted && (docId != null)) { // Didn't specify a revision to delete: 404 or a 409, depending if (ExistsDocumentWithIDAndRev(docId, null)) { throw new CouchbaseLiteException(StatusCode.Conflict); } else { throw new CouchbaseLiteException(StatusCode.NotFound); } } // Validate: ValidateRevision(oldRev, null, null); if (docId != null) { // Inserting first revision, with docID given (PUT): if (docNumericID <= 0) { // Doc doesn't exist at all; create it: docNumericID = InsertDocumentID(docId); if (docNumericID <= 0) { return null; } } else { // Doc ID exists; check whether current winning revision is deleted: if (oldWinnerWasDeletion) { prevRevId = oldWinningRevID; parentSequence = GetSequenceOfDocument(docNumericID, prevRevId, false); } else { if (oldWinningRevID != null) { // The current winning revision is not deleted, so this is a conflict throw new CouchbaseLiteException(StatusCode.Conflict); } } } } else { // Inserting first revision, with no docID given (POST): generate a unique docID: docId = Database.GenerateDocumentId(); docNumericID = InsertDocumentID(docId); if (docNumericID <= 0) { return null; } } } // There may be a conflict if (a) the document was already in conflict, or // (b) a conflict is created by adding a non-deletion child of a non-winning rev. inConflict = wasConflicted || (!deleted && prevRevId != null && oldWinningRevID != null && !prevRevId.Equals(oldWinningRevID)); // PART II: In which we prepare for insertion... // Get the attachments: var attachments = GetAttachmentsFromRevision(oldRev); // Bump the revID and update the JSON: IList<byte> json = null; if(!oldRev.IsDeleted()) //oldRev.GetProperties() != null && oldRev.GetProperties().Any()) { json = EncodeDocumentJSON(oldRev).ToList(); if (json == null) { // bad or missing json throw new CouchbaseLiteException(StatusCode.BadRequest); } if (json.Count() == 2 && json[0] == '{' && json[1] == '}') { json = null; } } else { json = Encoding.UTF8.GetBytes("{}"); // NOTE.ZJG: Confirm w/ Traun. This prevents a null reference exception in call to InsertRevision below. } var newRevId = GenerateIDForRevision(oldRev, json, attachments, prevRevId); newRev = oldRev.CopyWithDocID(docId, newRevId); StubOutAttachmentsInRevision(attachments, newRev); // Now insert the rev itself: var newSequence = InsertRevision(newRev, docNumericID, parentSequence, true, (attachments.Count > 0), json); if (newSequence == 0) { return null; } // Make replaced rev non-current: try { var args = new ContentValues(); args["current"] = 0; StorageEngine.Update("revs", args, "sequence=?", new[] { parentSequence.ToString() }); } catch (SQLException e) { Log.E(Database.Tag, "Error setting parent rev non-current", e); throw new CouchbaseLiteException(StatusCode.InternalServerError); } // Store any attachments: if (attachments != null) { ProcessAttachmentsForRevision(attachments, newRev, parentSequence); } // Figure out what the new winning rev ID is: winningRev = Winner(docNumericID, oldWinningRevID, oldWinnerWasDeletion, newRev); // Success! if (deleted) { resultStatus.SetCode(StatusCode.Ok); } else { resultStatus.SetCode(StatusCode.Created); } } catch (SQLException e1) { Log.E(Tag, "Error putting revision", e1); return null; } finally { if (cursor != null) { cursor.Close(); } EndTransaction(resultStatus.IsSuccessful); if (!string.IsNullOrEmpty(docId)) { UnsavedRevisionDocumentCache.Remove(docId); } } // EPILOGUE: A change notification is sent... NotifyChange(newRev, winningRev, null, inConflict); return newRev; }
public void TestRevisionWithNull() { RevisionInternal revisionWitDeletedNull = new RevisionInternal(new Dictionary<string, Object> { {"_id", Guid.NewGuid().ToString()}, {"_rev", "1-23243234"}, {"_deleted", null} }); RevisionInternal revisionWithDeletedFalse = new RevisionInternal(new Dictionary<string, Object> { {"_id", Guid.NewGuid().ToString()}, {"_rev", "1-23243234"}, {"_deleted", false} }); RevisionInternal revisionWithDeletedTrue = new RevisionInternal(new Dictionary<string, Object> { {"_id", Guid.NewGuid().ToString()}, {"_rev", "1-23243234"}, {"_deleted", true} }); RevisionInternal revisionWithDeletedString = new RevisionInternal(new Dictionary<string, Object> { {"_id", Guid.NewGuid().ToString()}, {"_rev", "1-23243234"}, {"_deleted", "foo"} }); Assert.IsFalse(revisionWitDeletedNull.IsDeleted()); Assert.IsFalse(revisionWithDeletedFalse.IsDeleted()); Assert.IsFalse(revisionWithDeletedString.IsDeleted()); Assert.IsTrue(revisionWithDeletedTrue.IsDeleted()); }
/// <summary>Inserts the _id, _rev and _attachments properties into the JSON data and stores it in rev. /// </summary> /// <remarks> /// Inserts the _id, _rev and _attachments properties into the JSON data and stores it in rev. /// Rev must already have its revID and sequence properties set. /// </remarks> internal IDictionary<String, Object> ExtraPropertiesForRevision(RevisionInternal rev, DocumentContentOptions contentOptions) { var docId = rev.GetDocId(); var revId = rev.GetRevId(); var sequenceNumber = rev.GetSequence(); Debug.Assert((revId != null)); Debug.Assert((sequenceNumber > 0)); // Get attachment metadata, and optionally the contents: IDictionary<string, object> attachmentsDict = null; if (!contentOptions.HasFlag(DocumentContentOptions.NoAttachments)) { attachmentsDict = GetAttachmentsDictForSequenceWithContent (sequenceNumber, contentOptions); } // Get more optional stuff to put in the properties: //OPT: This probably ends up making redundant SQL queries if multiple options are enabled. var localSeq = -1L; if (contentOptions.HasFlag(DocumentContentOptions.IncludeLocalSeq)) { localSeq = sequenceNumber; } IDictionary<string, object> revHistory = null; if (contentOptions.HasFlag(DocumentContentOptions.IncludeRevs)) { revHistory = GetRevisionHistoryDict(rev); } IList<object> revsInfo = null; if (contentOptions.HasFlag(DocumentContentOptions.IncludeRevsInfo)) { revsInfo = new AList<object>(); var revHistoryFull = GetRevisionHistory(rev); foreach (RevisionInternal historicalRev in revHistoryFull) { var revHistoryItem = new Dictionary<string, object>(); var status = "available"; if (historicalRev.IsDeleted()) { status = "deleted"; } if (historicalRev.IsMissing()) { status = "missing"; } revHistoryItem.Put("rev", historicalRev.GetRevId()); revHistoryItem["status"] = status; revsInfo.AddItem(revHistoryItem); } } IList<string> conflicts = null; if (contentOptions.HasFlag(DocumentContentOptions.IncludeConflicts)) { var revs = GetAllRevisionsOfDocumentID(docId, true); if (revs.Count > 1) { conflicts = new AList<string>(); foreach (RevisionInternal savedRev in revs) { if (!(savedRev.Equals(rev) || savedRev.IsDeleted())) { conflicts.AddItem(savedRev.GetRevId()); } } } } var result = new Dictionary<string, object>(); result["_id"] = docId; result["_rev"] = revId; if (rev.IsDeleted()) { result["_deleted"] = true; } if (attachmentsDict != null) { result["_attachments"] = attachmentsDict; } if (localSeq > -1) { result["_local_seq"] = localSeq; } if (revHistory != null) { result["_revisions"] = revHistory; } if (revsInfo != null) { result["_revs_info"] = revsInfo; } if (conflicts != null) { result["_conflicts"] = conflicts; } return result; }
/// <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; } } } } }
/// <summary> /// Given a revision, read its _attachments dictionary (if any), convert each attachment to a /// AttachmentInternal object, and return a dictionary mapping names->CBL_Attachments. /// </summary> /// <remarks> /// Given a revision, read its _attachments dictionary (if any), convert each attachment to a /// AttachmentInternal object, and return a dictionary mapping names->CBL_Attachments. /// </remarks> /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal IDictionary<String, AttachmentInternal> GetAttachmentsFromRevision(RevisionInternal rev) { var revAttachments = rev.GetPropertyForKey("_attachments").AsDictionary<string, object>(); if (revAttachments == null || revAttachments.Count == 0 || rev.IsDeleted()) { return new Dictionary<string, AttachmentInternal>(); } var attachments = new Dictionary<string, AttachmentInternal>(); foreach (var name in revAttachments.Keys) { var attachInfo = revAttachments.Get(name).AsDictionary<string, object>(); var contentType = (string)attachInfo.Get("content_type"); var attachment = new AttachmentInternal(name, contentType); var newContentBase64 = (string)attachInfo.Get("data"); if (newContentBase64 != null) { // If there's inline attachment data, decode and store it: byte[] newContents; try { newContents = StringUtils.ConvertFromUnpaddedBase64String (newContentBase64); } catch (IOException e) { throw new CouchbaseLiteException(e, StatusCode.BadEncoding); } attachment.Length = newContents.Length; var outBlobKey = new BlobKey(); var storedBlob = Attachments.StoreBlob(newContents, outBlobKey); attachment.BlobKey = outBlobKey; if (!storedBlob) { throw new CouchbaseLiteException(StatusCode.AttachmentError); } } else { if (attachInfo.ContainsKey("follows") && ((bool)attachInfo.Get("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 { // This item is just a stub; validate and skip it if (((bool)attachInfo.Get("stub")) == false) { throw new CouchbaseLiteException("Expected this attachment to be a stub", StatusCode. BadAttachment); } var revPos = Convert.ToInt64(attachInfo.Get("revpos")); if (revPos <= 0) { throw new CouchbaseLiteException("Invalid revpos: " + revPos, StatusCode.BadAttachment); } continue; } } // Handle encoded attachment: string encodingStr = (string)attachInfo.Get("encoding"); if (encodingStr != null && encodingStr.Length > 0) { if (Runtime.EqualsIgnoreCase(encodingStr, "gzip")) { attachment.Encoding = AttachmentEncoding.GZIP; } else { throw new CouchbaseLiteException("Unnkown encoding: " + encodingStr, StatusCode.BadEncoding ); } attachment.EncodedLength = attachment.Length; if (attachInfo.ContainsKey("length")) { attachment.Length = attachInfo.GetCast<long>("length"); } } if (attachInfo.ContainsKey("revpos")) { var revpos = Convert.ToInt32(attachInfo.Get("revpos")); attachment.RevPos = revpos; } attachments[name] = attachment; } return attachments; }
internal Int64 InsertRevision(RevisionInternal rev, long docNumericID, long parentSequence, bool current, bool hasAttachments, IEnumerable<byte> data) { var rowId = 0L; try { var args = new ContentValues(); args["doc_id"] = docNumericID; args.Put("revid", rev.GetRevId()); if (parentSequence != 0) { args["parent"] = parentSequence; } args["current"] = current; args["deleted"] = rev.IsDeleted(); args["no_attachments"] = !hasAttachments; if (data != null) { args["json"] = data.ToArray(); } rowId = StorageEngine.Insert("revs", null, args); rev.SetSequence(rowId); } catch (Exception e) { Log.E(Tag, "Error inserting revision", e); } return rowId; }
internal RevisionInternal PutLocalRevision(RevisionInternal revision, string prevRevID) { var docID = revision.GetDocId(); if (!docID.StartsWith ("_local/", StringComparison.InvariantCultureIgnoreCase)) { throw new CouchbaseLiteException(StatusCode.BadRequest); } if (!revision.IsDeleted()) { // PUT: string newRevID; var json = EncodeDocumentJSON(revision); if (prevRevID != null) { var generation = RevisionInternal.GenerationFromRevID(prevRevID); if (generation == 0) { throw new CouchbaseLiteException(StatusCode.BadRequest); } newRevID = Sharpen.Extensions.ToString(++generation) + "-local"; var values = new ContentValues(); values["revid"] = newRevID; values["json"] = json; var whereArgs = new [] { docID, prevRevID }; try { var rowsUpdated = StorageEngine.Update("localdocs", values, "docid=? AND revid=?", whereArgs); if (rowsUpdated == 0) { throw new CouchbaseLiteException(StatusCode.Conflict); } } catch (SQLException e) { throw new CouchbaseLiteException(e, StatusCode.InternalServerError); } } else { newRevID = "1-local"; var values = new ContentValues(); values["docid"] = docID; values["revid"] = newRevID; values["json"] = json; try { StorageEngine.InsertWithOnConflict("localdocs", null, values, ConflictResolutionStrategy.Ignore); } catch (SQLException e) { throw new CouchbaseLiteException(e, StatusCode.InternalServerError); } } return revision.CopyWithDocID(docID, newRevID); } else { // DELETE: DeleteLocalDocument(docID, prevRevID); return revision; } }
private static void VerifyHistory(Database db, RevisionInternal rev, IList<string > history) { RevisionInternal gotRev = db.GetDocumentWithIDAndRev(rev.GetDocId(), null, EnumSet .NoneOf<Database.TDContentOptions>()); NUnit.Framework.Assert.AreEqual(rev, gotRev); NUnit.Framework.Assert.AreEqual(rev.GetProperties(), gotRev.GetProperties()); IList<RevisionInternal> revHistory = db.GetRevisionHistory(gotRev); NUnit.Framework.Assert.AreEqual(history.Count, revHistory.Count); for (int i = 0; i < history.Count; i++) { RevisionInternal hrev = revHistory[i]; NUnit.Framework.Assert.AreEqual(rev.GetDocId(), hrev.GetDocId()); NUnit.Framework.Assert.AreEqual(history[i], hrev.GetRevId()); NUnit.Framework.Assert.IsFalse(rev.IsDeleted()); } }
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(); }); }
internal IDictionary<string, AttachmentInternal> GetAttachmentsFromRevision(RevisionInternal rev) { IDictionary<string, object> revAttachments = (IDictionary<string, object>)rev.GetPropertyForKey ("_attachments"); if (revAttachments == null || revAttachments.Count == 0 || rev.IsDeleted()) { return new Dictionary<string, AttachmentInternal>(); } IDictionary<string, AttachmentInternal> attachments = new Dictionary<string, AttachmentInternal >(); foreach (string name in revAttachments.Keys) { IDictionary<string, object> attachInfo = (IDictionary<string, object>)revAttachments .Get(name); string contentType = (string)attachInfo.Get("content_type"); AttachmentInternal attachment = new AttachmentInternal(name, contentType); string newContentBase64 = (string)attachInfo.Get("data"); if (newContentBase64 != null) { // If there's inline attachment data, decode and store it: byte[] newContents; try { newContents = Base64.Decode(newContentBase64); } catch (IOException e) { throw new CouchbaseLiteException(e, Status.BadEncoding); } attachment.SetLength(newContents.Length); BlobKey outBlobKey = new BlobKey(); bool storedBlob = GetAttachments().StoreBlob(newContents, outBlobKey); attachment.SetBlobKey(outBlobKey); if (!storedBlob) { throw new CouchbaseLiteException(Status.StatusAttachmentError); } } else { if (attachInfo.ContainsKey("follows") && ((bool)attachInfo.Get("follows")) == true) { // "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 { // This item is just a stub; validate and skip it if (((bool)attachInfo.Get("stub")) == false) { throw new CouchbaseLiteException("Expected this attachment to be a stub", Status. BadAttachment); } int revPos = ((int)attachInfo.Get("revpos")); if (revPos <= 0) { throw new CouchbaseLiteException("Invalid revpos: " + revPos, Status.BadAttachment ); } continue; } } // Handle encoded attachment: string encodingStr = (string)attachInfo.Get("encoding"); if (encodingStr != null && encodingStr.Length > 0) { if (Sharpen.Runtime.EqualsIgnoreCase(encodingStr, "gzip")) { attachment.SetEncoding(AttachmentInternal.AttachmentEncoding.AttachmentEncodingGZIP ); } else { throw new CouchbaseLiteException("Unnkown encoding: " + encodingStr, Status.BadEncoding ); } attachment.SetEncodedLength(attachment.GetLength()); if (attachInfo.ContainsKey("length")) { Number attachmentLength = (Number)attachInfo.Get("length"); attachment.SetLength(attachmentLength); } } if (attachInfo.ContainsKey("revpos")) { attachment.SetRevpos((int)attachInfo.Get("revpos")); } else { attachment.SetRevpos(1); } attachments.Put(name, attachment); } return attachments; }
public RevisionInternal PutLocalRevision(RevisionInternal revision, string prevRevID ) { string docID = revision.GetDocId(); if (!docID.StartsWith("_local/")) { throw new CouchbaseLiteException(Status.BadRequest); } if (!revision.IsDeleted()) { // PUT: byte[] json = EncodeDocumentJSON(revision); string newRevID; if (prevRevID != null) { int generation = RevisionInternal.GenerationFromRevID(prevRevID); if (generation == 0) { throw new CouchbaseLiteException(Status.BadRequest); } newRevID = Sharpen.Extensions.ToString(++generation) + "-local"; ContentValues values = new ContentValues(); values.Put("revid", newRevID); values.Put("json", json); string[] whereArgs = new string[] { docID, prevRevID }; try { int rowsUpdated = database.Update("localdocs", values, "docid=? AND revid=?", whereArgs ); if (rowsUpdated == 0) { throw new CouchbaseLiteException(Status.Conflict); } } catch (SQLException e) { throw new CouchbaseLiteException(e, Status.InternalServerError); } } else { newRevID = "1-local"; ContentValues values = new ContentValues(); values.Put("docid", docID); values.Put("revid", newRevID); values.Put("json", json); try { database.InsertWithOnConflict("localdocs", null, values, SQLiteStorageEngine.ConflictIgnore ); } catch (SQLException e) { throw new CouchbaseLiteException(e, Status.InternalServerError); } } return revision.CopyWithDocID(docID, newRevID); } else { // DELETE: DeleteLocalDocument(docID, prevRevID); return revision; } }
public RevisionInternal PutRevision(RevisionInternal oldRev, string prevRevId, bool allowConflict, Status resultStatus) { // prevRevId is the rev ID being replaced, or nil if an insert string docId = oldRev.GetDocId(); bool deleted = oldRev.IsDeleted(); if ((oldRev == null) || ((prevRevId != null) && (docId == null)) || (deleted && ( docId == null)) || ((docId != null) && !IsValidDocumentId(docId))) { throw new CouchbaseLiteException(Status.BadRequest); } BeginTransaction(); Cursor cursor = null; bool inConflict = false; RevisionInternal winningRev = null; RevisionInternal newRev = null; //// PART I: In which are performed lookups and validations prior to the insert... long docNumericID = (docId != null) ? GetDocNumericID(docId) : 0; long parentSequence = 0; string oldWinningRevID = null; try { bool oldWinnerWasDeletion = false; bool wasConflicted = false; if (docNumericID > 0) { IList<bool> outIsDeleted = new AList<bool>(); IList<bool> outIsConflict = new AList<bool>(); try { oldWinningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict); if (outIsDeleted.Count > 0) { oldWinnerWasDeletion = true; } if (outIsConflict.Count > 0) { wasConflicted = true; } } catch (Exception e) { Sharpen.Runtime.PrintStackTrace(e); } } if (prevRevId != null) { // Replacing: make sure given prevRevID is current & find its sequence number: if (docNumericID <= 0) { string msg = string.Format("No existing revision found with doc id: %s", docId); throw new CouchbaseLiteException(msg, Status.NotFound); } string[] args = new string[] { System.Convert.ToString(docNumericID), prevRevId }; string additionalWhereClause = string.Empty; if (!allowConflict) { additionalWhereClause = "AND current=1"; } cursor = database.RawQuery("SELECT sequence FROM revs WHERE doc_id=? AND revid=? " + additionalWhereClause + " LIMIT 1", args); if (cursor.MoveToNext()) { parentSequence = cursor.GetLong(0); } if (parentSequence == 0) { // Not found: either a 404 or a 409, depending on whether there is any current revision if (!allowConflict && ExistsDocumentWithIDAndRev(docId, null)) { string msg = string.Format("Conflicts not allowed and there is already an existing doc with id: %s" , docId); throw new CouchbaseLiteException(msg, Status.Conflict); } else { string msg = string.Format("No existing revision found with doc id: %s", docId); throw new CouchbaseLiteException(msg, Status.NotFound); } } if (validations != null && validations.Count > 0) { // Fetch the previous revision and validate the new one against it: RevisionInternal prevRev = new RevisionInternal(docId, prevRevId, false, this); ValidateRevision(oldRev, prevRev); } // Make replaced rev non-current: ContentValues updateContent = new ContentValues(); updateContent.Put("current", 0); database.Update("revs", updateContent, "sequence=" + parentSequence, null); } else { // Inserting first revision. if (deleted && (docId != null)) { // Didn't specify a revision to delete: 404 or a 409, depending if (ExistsDocumentWithIDAndRev(docId, null)) { throw new CouchbaseLiteException(Status.Conflict); } else { throw new CouchbaseLiteException(Status.NotFound); } } // Validate: ValidateRevision(oldRev, null); if (docId != null) { // Inserting first revision, with docID given (PUT): if (docNumericID <= 0) { // Doc doesn't exist at all; create it: docNumericID = InsertDocumentID(docId); if (docNumericID <= 0) { return null; } } else { // Doc ID exists; check whether current winning revision is deleted: if (oldWinnerWasDeletion == true) { prevRevId = oldWinningRevID; parentSequence = GetSequenceOfDocument(docNumericID, prevRevId, false); } else { if (oldWinningRevID != null) { // The current winning revision is not deleted, so this is a conflict throw new CouchbaseLiteException(Status.Conflict); } } } } else { // Inserting first revision, with no docID given (POST): generate a unique docID: docId = Database.GenerateDocumentId(); docNumericID = InsertDocumentID(docId); if (docNumericID <= 0) { return null; } } } // There may be a conflict if (a) the document was already in conflict, or // (b) a conflict is created by adding a non-deletion child of a non-winning rev. inConflict = wasConflicted || (!deleted && prevRevId != null && oldWinningRevID != null && !prevRevId.Equals(oldWinningRevID)); //// PART II: In which insertion occurs... // Get the attachments: IDictionary<string, AttachmentInternal> attachments = GetAttachmentsFromRevision( oldRev); // Bump the revID and update the JSON: string newRevId = GenerateNextRevisionID(prevRevId); byte[] data = null; if (!oldRev.IsDeleted()) { data = EncodeDocumentJSON(oldRev); if (data == null) { // bad or missing json throw new CouchbaseLiteException(Status.BadRequest); } } newRev = oldRev.CopyWithDocID(docId, newRevId); StubOutAttachmentsInRevision(attachments, newRev); // Now insert the rev itself: long newSequence = InsertRevision(newRev, docNumericID, parentSequence, true, data ); if (newSequence == 0) { return null; } // Store any attachments: if (attachments != null) { ProcessAttachmentsForRevision(attachments, newRev, parentSequence); } // Figure out what the new winning rev ID is: winningRev = Winner(docNumericID, oldWinningRevID, oldWinnerWasDeletion, newRev); // Success! if (deleted) { resultStatus.SetCode(Status.Ok); } else { resultStatus.SetCode(Status.Created); } } catch (SQLException e1) { Log.E(Database.Tag, "Error putting revision", e1); return null; } finally { if (cursor != null) { cursor.Close(); } EndTransaction(resultStatus.IsSuccessful()); } //// EPILOGUE: A change notification is sent... NotifyChange(newRev, winningRev, null, inConflict); return newRev; }
public long InsertRevision(RevisionInternal rev, long docNumericID, long parentSequence , bool current, byte[] data) { long rowId = 0; try { ContentValues args = new ContentValues(); args.Put("doc_id", docNumericID); args.Put("revid", rev.GetRevId()); if (parentSequence != 0) { args.Put("parent", parentSequence); } args.Put("current", current); args.Put("deleted", rev.IsDeleted()); args.Put("json", data); rowId = database.Insert("revs", null, args); rev.SetSequence(rowId); } catch (Exception e) { Log.E(Database.Tag, "Error inserting revision", e); } return rowId; }
public IDictionary<string, object> ExtraPropertiesForRevision(RevisionInternal rev , EnumSet<Database.TDContentOptions> contentOptions) { string docId = rev.GetDocId(); string revId = rev.GetRevId(); long sequenceNumber = rev.GetSequence(); System.Diagnostics.Debug.Assert((revId != null)); System.Diagnostics.Debug.Assert((sequenceNumber > 0)); // Get attachment metadata, and optionally the contents: IDictionary<string, object> attachmentsDict = GetAttachmentsDictForSequenceWithContent (sequenceNumber, contentOptions); // Get more optional stuff to put in the properties: //OPT: This probably ends up making redundant SQL queries if multiple options are enabled. long localSeq = null; if (contentOptions.Contains(Database.TDContentOptions.TDIncludeLocalSeq)) { localSeq = sequenceNumber; } IDictionary<string, object> revHistory = null; if (contentOptions.Contains(Database.TDContentOptions.TDIncludeRevs)) { revHistory = GetRevisionHistoryDict(rev); } IList<object> revsInfo = null; if (contentOptions.Contains(Database.TDContentOptions.TDIncludeRevsInfo)) { revsInfo = new AList<object>(); IList<RevisionInternal> revHistoryFull = GetRevisionHistory(rev); foreach (RevisionInternal historicalRev in revHistoryFull) { IDictionary<string, object> revHistoryItem = new Dictionary<string, object>(); string status = "available"; if (historicalRev.IsDeleted()) { status = "deleted"; } if (historicalRev.IsMissing()) { status = "missing"; } revHistoryItem.Put("rev", historicalRev.GetRevId()); revHistoryItem.Put("status", status); revsInfo.AddItem(revHistoryItem); } } IList<string> conflicts = null; if (contentOptions.Contains(Database.TDContentOptions.TDIncludeConflicts)) { RevisionList revs = GetAllRevisionsOfDocumentID(docId, true); if (revs.Count > 1) { conflicts = new AList<string>(); foreach (RevisionInternal historicalRev in revs) { if (!historicalRev.Equals(rev)) { conflicts.AddItem(historicalRev.GetRevId()); } } } } IDictionary<string, object> result = new Dictionary<string, object>(); result.Put("_id", docId); result.Put("_rev", revId); if (rev.IsDeleted()) { result.Put("_deleted", true); } if (attachmentsDict != null) { result.Put("_attachments", attachmentsDict); } if (localSeq != null) { result.Put("_local_seq", localSeq); } if (revHistory != null) { result.Put("_revisions", revHistory); } if (revsInfo != null) { result.Put("_revs_info", revsInfo); } if (conflicts != null) { result.Put("_conflicts", conflicts); } return result; }
internal RevisionInternal Winner(Int64 docNumericID, String oldWinningRevID, Boolean oldWinnerWasDeletion, RevisionInternal newRev) { if (oldWinningRevID == null) { return newRev; } var newRevID = newRev.GetRevId(); if (!newRev.IsDeleted()) { if (oldWinnerWasDeletion || RevisionInternal.CBLCompareRevIDs(newRevID, oldWinningRevID) > 0) { return newRev; } } else { // this is now the winning live revision if (oldWinnerWasDeletion) { if (RevisionInternal.CBLCompareRevIDs(newRevID, oldWinningRevID) > 0) { return newRev; } } else { // doc still deleted, but this beats previous deletion rev // Doc was alive. How does this deletion affect the winning rev ID? var outIsDeleted = new AList<bool>(); var outIsConflict = new AList<bool>(); var winningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict); if (!winningRevID.Equals(oldWinningRevID)) { if (winningRevID.Equals(newRev.GetRevId())) { return newRev; } else { var deleted = false; var winningRev = new RevisionInternal(newRev.GetDocId(), winningRevID, deleted, this); return winningRev; } } } } return null; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestValidations() { Validator validator = new _Validator_19(this); database.SetValidation("hoopy", validator); // POST a valid new document: IDictionary<string, object> props = new Dictionary<string, object>(); props.Put("name", "Zaphod Beeblebrox"); props.Put("towel", "velvet"); RevisionInternal rev = new RevisionInternal(props, database); Status status = new Status(); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); // PUT a valid update: props.Put("head_count", 3); rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(Status.Created, status.GetCode()); // PUT an invalid update: Sharpen.Collections.Remove(props, "towel"); rev.SetProperties(props); validationCalled = false; bool gotExpectedError = false; try { rev = database.PutRevision(rev, rev.GetRevId(), false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == Status.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // POST an invalid new document: props = new Dictionary<string, object>(); props.Put("name", "Vogon"); props.Put("poetry", true); rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == Status.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // PUT a valid new document with an ID: props = new Dictionary<string, object>(); props.Put("_id", "ford"); props.Put("name", "Ford Prefect"); props.Put("towel", "terrycloth"); rev = new RevisionInternal(props, database); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual("ford", rev.GetDocId()); // DELETE a document: rev = new RevisionInternal(rev.GetDocId(), rev.GetRevId(), true, database); NUnit.Framework.Assert.IsTrue(rev.IsDeleted()); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); // PUT an invalid new document: props = new Dictionary<string, object>(); props.Put("_id", "petunias"); props.Put("name", "Pot of Petunias"); rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == Status.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); }
/// <summary> /// 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); } } }
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); } }
internal String GenerateIDForRevision(RevisionInternal rev, IEnumerable<byte> json, IDictionary<string, AttachmentInternal> attachments, string previousRevisionId) { MessageDigest md5Digest; // Revision IDs have a generation count, a hyphen, and a UUID. int generation = 0; if (previousRevisionId != null) { generation = RevisionInternal.GenerationFromRevID(previousRevisionId); if (generation == 0) { return null; } } // Generate a digest for this revision based on the previous revision ID, document JSON, // and attachment digests. This doesn't need to be secure; we just need to ensure that this // code consistently generates the same ID given equivalent revisions. try { md5Digest = MessageDigest.GetInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } var length = 0; if (previousRevisionId != null) { var prevIDUTF8 = Encoding.UTF8.GetBytes(previousRevisionId); length = prevIDUTF8.Length; } if (length > unchecked((0xFF))) { return null; } var lengthByte = unchecked((byte)(length & unchecked((0xFF)))); var lengthBytes = new[] { lengthByte }; md5Digest.Update(lengthBytes); var isDeleted = ((rev.IsDeleted()) ? 1 : 0); var deletedByte = new[] { unchecked((byte)isDeleted) }; md5Digest.Update(deletedByte); var attachmentKeys = new List<String>(attachments.Keys); attachmentKeys.Sort(); foreach (string key in attachmentKeys) { var attachment = attachments.Get(key); md5Digest.Update(attachment.GetBlobKey().GetBytes()); } if (json != null) { md5Digest.Update(json != null ? json.ToArray() : null); } var md5DigestResult = md5Digest.Digest(); var digestAsHex = BitConverter.ToString(md5DigestResult).Replace("-", String.Empty); int generationIncremented = generation + 1; return string.Format("{0}-{1}", generationIncremented, digestAsHex); }
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; }
/// <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.SetSequence(rev.GetSequence()); rev = rev2; } } return new NonNullDictionary<string, object> { { "seq", rev.GetSequence() }, { "id", rev.GetDocId() }, { "changes", new List<object> { new Dictionary<string, object> { { "rev", rev.GetRevId() } } } }, { "deleted", rev.IsDeleted() ? (object)true : null }, { "doc", responseState.ChangesIncludeDocs ? rev.GetProperties() : null } }; }
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()); } }
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(); }); }
/// <summary>Inserts an already-existing revision replicated from a remote sqliteDb.</summary> /// <remarks> /// Inserts an already-existing revision replicated from a remote sqliteDb. /// It must already have a revision ID. This may create a conflict! The revision's history must be given; ancestor revision IDs that don't already exist locally will create phantom revisions with no content. /// </remarks> /// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> internal void ForceInsert(RevisionInternal rev, IList<string> revHistory, Uri source) { var inConflict = false; var docId = rev.GetDocId(); var revId = rev.GetRevId(); if (!IsValidDocumentId(docId) || (revId == null)) { throw new CouchbaseLiteException(StatusCode.BadRequest); } int historyCount = 0; if (revHistory != null) { historyCount = revHistory.Count; } if (historyCount == 0) { revHistory = new AList<string>(); revHistory.AddItem(revId); historyCount = 1; } else { if (!revHistory[0].Equals(rev.GetRevId())) { throw new CouchbaseLiteException(StatusCode.BadRequest); } } bool success = false; BeginTransaction(); try { // First look up all locally-known revisions of this document: long docNumericID = GetOrInsertDocNumericID(docId); RevisionList localRevs = GetAllRevisionsOfDocumentID(docId, docNumericID, false); if (localRevs == null) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } IList<bool> outIsDeleted = new AList<bool>(); IList<bool> outIsConflict = new AList<bool>(); bool oldWinnerWasDeletion = false; string oldWinningRevID = WinningRevIDOfDoc(docNumericID, outIsDeleted, outIsConflict ); if (outIsDeleted.Count > 0) { oldWinnerWasDeletion = true; } if (outIsConflict.Count > 0) { inConflict = true; } // Walk through the remote history in chronological order, matching each revision ID to // a local revision. When the list diverges, start creating blank local revisions to fill // in the local history: long sequence = 0; long localParentSequence = 0; for (int i = revHistory.Count - 1; i >= 0; --i) { revId = revHistory[i]; RevisionInternal localRev = localRevs.RevWithDocIdAndRevId(docId, revId); if (localRev != null) { // This revision is known locally. Remember its sequence as the parent of the next one: sequence = localRev.GetSequence(); Debug.Assert((sequence > 0)); localParentSequence = sequence; } else { // This revision isn't known, so add it: RevisionInternal newRev; IEnumerable<Byte> data = null; bool current = false; if (i == 0) { // Hey, this is the leaf revision we're inserting: newRev = rev; if (!rev.IsDeleted()) { data = EncodeDocumentJSON(rev); if (data == null) { throw new CouchbaseLiteException(StatusCode.BadRequest); } } current = true; } else { // It's an intermediate parent, so insert a stub: newRev = new RevisionInternal(docId, revId, false, this); } // Insert it: sequence = InsertRevision(newRev, docNumericID, sequence, current, (GetAttachmentsFromRevision(newRev).Count > 0), data); if (sequence <= 0) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } if (i == 0) { // Write any changed attachments for the new revision. As the parent sequence use // the latest local revision (this is to copy attachments from): var attachments = GetAttachmentsFromRevision(rev); if (attachments != null) { ProcessAttachmentsForRevision(attachments, rev, localParentSequence); StubOutAttachmentsInRevision(attachments, rev); } } } } // Mark the latest local rev as no longer current: if (localParentSequence > 0 && localParentSequence != sequence) { ContentValues args = new ContentValues(); args["current"] = 0; string[] whereArgs = new string[] { Convert.ToString(localParentSequence) }; try { var numRowsChanged = StorageEngine.Update("revs", args, "sequence=?", whereArgs); if (numRowsChanged == 0) { inConflict = true; } } catch (Exception) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } } var winningRev = Winner(docNumericID, oldWinningRevID, oldWinnerWasDeletion, rev); success = true; NotifyChange(rev, winningRev, source, inConflict); } catch (SQLException) { throw new CouchbaseLiteException(StatusCode.InternalServerError); } finally { EndTransaction(success); } }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException"></exception> public virtual void TestValidations() { Database.ValidateDelegate validator = (Revision newRevision, ValidationContext context)=> { NUnit.Framework.Assert.IsNotNull(newRevision); NUnit.Framework.Assert.IsNotNull(context); NUnit.Framework.Assert.IsTrue(newRevision.Properties != null || newRevision. IsDeletion); this._enclosing.validationCalled = true; bool hoopy = newRevision.IsDeletion || (newRevision.Properties.Get("towel" ) != null); Log.V(ValidationsTest.Tag, string.Format("--- Validating %s --> %b", newRevision. Properties, hoopy)); if (!hoopy) { context.Reject("Where's your towel?"); } return hoopy; }; database.SetValidation("hoopy", validator); // POST a valid new document: IDictionary<string, object> props = new Dictionary<string, object>(); props["name"] = "Zaphod Beeblebrox"; props["towel"] = "velvet"; RevisionInternal rev = new RevisionInternal(props, database); Status status = new Status(); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(StatusCode.Created, status.GetCode()); // PUT a valid update: props["head_count"] = 3; rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual(StatusCode.Created, status.GetCode()); // PUT an invalid update: Sharpen.Collections.Remove(props, "towel"); rev.SetProperties(props); validationCalled = false; bool gotExpectedError = false; try { rev = database.PutRevision(rev, rev.GetRevId(), false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // POST an invalid new document: props = new Dictionary<string, object>(); props["name"] = "Vogon"; props["poetry"] = true; rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); // PUT a valid new document with an ID: props = new Dictionary<string, object>(); props["_id"] = "ford"; props["name"] = "Ford Prefect"; props["towel"] = "terrycloth"; rev = new RevisionInternal(props, database); validationCalled = false; rev = database.PutRevision(rev, null, false, status); NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.AreEqual("ford", rev.GetDocId()); // DELETE a document: rev = new RevisionInternal(rev.GetDocId(), rev.GetRevId(), true, database); NUnit.Framework.Assert.IsTrue(rev.IsDeleted()); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); NUnit.Framework.Assert.IsTrue(validationCalled); // PUT an invalid new document: props = new Dictionary<string, object>(); props["_id"] = "petunias"; props["name"] = "Pot of Petunias"; rev = new RevisionInternal(props, database); validationCalled = false; gotExpectedError = false; try { rev = database.PutRevision(rev, null, false, status); } catch (CouchbaseLiteException e) { gotExpectedError = (e.GetCBLStatus().GetCode() == StatusCode.Forbidden); } NUnit.Framework.Assert.IsTrue(validationCalled); NUnit.Framework.Assert.IsTrue(gotExpectedError); }
public void TestValidations() { ValidateDelegate validator = (newRevision, context)=> { Assert.IsNotNull(newRevision); Assert.IsNotNull(context); Assert.IsTrue(newRevision.Properties != null || newRevision.IsDeletion); validationCalled = true; bool hoopy = newRevision.IsDeletion || (newRevision.Properties.Get("towel") != null); Log.V(ValidationsTest.Tag, string.Format("--- Validating {0} --> {1}", newRevision.Properties, hoopy)); if (!hoopy) { context.Reject("Where's your towel?"); } return hoopy; }; database.SetValidation("hoopy", validator); // POST a valid new document: IDictionary<string, object> props = new Dictionary<string, object>(); props["name"] = "Zaphod Beeblebrox"; props["towel"] = "velvet"; RevisionInternal rev = new RevisionInternal(props); Status status = new Status(); validationCalled = false; rev = database.PutRevision(rev, null, false, status); Assert.IsTrue(validationCalled); Assert.AreEqual(StatusCode.Created, status.Code); // PUT a valid update: props["head_count"] = 3; rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); Assert.IsTrue(validationCalled); Assert.AreEqual(StatusCode.Created, status.Code); // PUT an invalid update: Sharpen.Collections.Remove(props, "towel"); rev.SetProperties(props); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); Assert.IsTrue(validationCalled); Assert.AreEqual(StatusCode.Forbidden, status.Code); // POST an invalid new document: props = new Dictionary<string, object>(); props["name"] = "Vogon"; props["poetry"] = true; rev = new RevisionInternal(props); validationCalled = false; rev = database.PutRevision(rev, null, false, status); Assert.IsTrue(validationCalled); Assert.AreEqual(StatusCode.Forbidden, status.Code); // PUT a valid new document with an ID: props = new Dictionary<string, object>(); props["_id"] = "ford"; props["name"] = "Ford Prefect"; props["towel"] = "terrycloth"; rev = new RevisionInternal(props); validationCalled = false; rev = database.PutRevision(rev, null, false, status); Assert.IsTrue(validationCalled); Assert.AreEqual("ford", rev.GetDocId()); // DELETE a document: rev = new RevisionInternal(rev.GetDocId(), rev.GetRevId(), true); Assert.IsTrue(rev.IsDeleted()); validationCalled = false; rev = database.PutRevision(rev, rev.GetRevId(), false, status); Assert.IsTrue(validationCalled); // PUT an invalid new document: props = new Dictionary<string, object>(); props["_id"] = "petunias"; props["name"] = "Pot of Petunias"; rev = new RevisionInternal(props); validationCalled = false; rev = database.PutRevision(rev, null, false, status); Assert.IsTrue(validationCalled); Assert.AreEqual(StatusCode.Forbidden, status.Code); }