Пример #1
0
        /// <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());
        }
Пример #3
0
        /// <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;
        }
Пример #4
0
        /// <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;
                        }
                    }
                }
            }
        }
Пример #5
0
        /// <summary>
        /// Given a revision, read its _attachments dictionary (if any), convert each attachment to a
        /// AttachmentInternal object, and return a dictionary mapping names-&gt;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-&gt;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;
        }
Пример #6
0
        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;
        }
Пример #7
0
        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;
            }
        }
Пример #8
0
		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());
			}
		}
Пример #9
0
        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();
            });
        }
Пример #10
0
		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;
		}
Пример #11
0
		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;
			}
		}
Пример #12
0
		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;
		}
Пример #13
0
		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;
		}
Пример #14
0
		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;
		}
Пример #15
0
        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;
        }
Пример #16
0
		/// <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);
		}
Пример #17
0
        /// <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);
            }
        }
Пример #19
0
        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 }
     };
 }
Пример #22
0
        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());
            }
        }
Пример #23
0
        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();
            });
        }
Пример #24
0
        /// <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);
            }
        }
Пример #25
0
		/// <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);
        }