/// <exception cref="Couchbase.Lite.CouchbaseLiteException">When attempting to add an invalid revision</exception> internal void ForceInsert(RevisionInternal inRev, IList<string> revHistory, Uri source) { if (revHistory == null) { revHistory = new List<string>(0); } var rev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId()); rev.SetSequence(0); string revID = rev.GetRevId(); if (!IsValidDocumentId(rev.GetDocId()) || revID == null) { throw new CouchbaseLiteException(StatusCode.BadId); } if (revHistory.Count == 0) { revHistory.Add(revID); } else if (revID != revHistory[0]) { throw new CouchbaseLiteException(StatusCode.BadId); } if (inRev.GetAttachments() != null) { var updatedRev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId()); string prevRevID = revHistory.Count >= 2 ? revHistory[1] : null; Status status = new Status(); if (!ProcessAttachmentsForRevision(updatedRev, prevRevID, status)) { throw new CouchbaseLiteException(status.Code); } inRev = updatedRev; } StoreValidation validationBlock = null; if (Shared != null && Shared.HasValues("validation", Name)) { validationBlock = ValidateRevision; } var insertStatus = Storage.ForceInsert(inRev, revHistory, validationBlock, source); if(insertStatus.IsError) { throw new CouchbaseLiteException(insertStatus.Code); } }
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; } }
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; }
/// <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 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; }
/// <exception cref="Couchbase.Lite.CouchbaseLiteException">When attempting to add an invalid revision</exception> internal void ForceInsert(RevisionInternal inRev, IList<string> revHistory, Uri source) { if (!IsOpen) { throw new CouchbaseLiteException("DB is closed", StatusCode.DbError); } if (revHistory == null) { revHistory = new List<string>(0); } var rev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId()); rev.SetSequence(0); string revID = rev.GetRevId(); if (!Document.IsValidDocumentId(rev.GetDocId()) || revID == null) { throw new CouchbaseLiteException(StatusCode.BadId); } if (revHistory.Count == 0) { revHistory.Add(revID); } else if (revID != revHistory[0]) { throw new CouchbaseLiteException(StatusCode.BadId); } if (inRev.GetAttachments() != null) { var updatedRev = inRev.CopyWithDocID(inRev.GetDocId(), inRev.GetRevId()); ProcessAttachmentsForRevision(updatedRev, revHistory.Skip(1).Take(revHistory.Count-1).ToList()); inRev = updatedRev; } StoreValidation validationBlock = null; if (Shared != null && Shared.HasValues("validation", Name)) { validationBlock = ValidateRevision; } Storage.ForceInsert(inRev, revHistory, validationBlock, source); }
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; }