Exemple #1
0
        /// <summary>
        /// Performs the given logic with the specified database
        /// </summary>
        /// <returns>The result (in terms of response to the client) of the database operation</returns>
        /// <param name="context">The Couchbase Lite HTTP context</param>
        /// <param name="open">Whether or not to open the database, or just find it</param>
        /// <param name="action">The logic to perform on the database</param>
        public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open,
                                                                     Func <Database, CouchbaseLiteResponse> action)
        {
            string   dbName = context.DatabaseName;
            Database db     = context.DbManager.GetDatabase(dbName, true);

            if (db == null)
            {
                return(context.CreateResponse(StatusCode.NotFound));
            }

            if (open)
            {
                try {
                    db.Open();
                } catch (CouchbaseLiteException e) {
                    Log.To.Listener.W(TAG, "Exception in PerformLogicWithDatabase, returning 500", e);
                    return(context.CreateResponse(StatusCode.DbError));
                } catch (Exception e) {
                    Log.To.Listener.E(TAG, "Exception in PerformLogicWithDatabase, returning 500", e);
                    return(context.CreateResponse(StatusCode.DbError));
                }
            }

            return(action(db));
        }
Exemple #2
0
        /// <summary>
        /// The POST to _all_docs allows to specify multiple keys to be selected from the database.
        /// This enables you to request multiple documents in a single request, in place of multiple GET /{db}/{docid} requests.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_all_docs
        /// <remarks>
        public static ICouchbaseResponseState GetAllSpecifiedDocuments(ICouchbaseListenerContext context)
        {
            return(PerformLogicWithDatabase(context, true, db =>
            {
                var options = context.QueryOptions;
                if (options == null)
                {
                    return context.CreateResponse(StatusCode.BadParam);
                }

                var body = context.BodyAs <Dictionary <string, object> >();
                if (body == null)
                {
                    return context.CreateResponse(StatusCode.BadJson);
                }

                if (!body.ContainsKey("keys"))
                {
                    return context.CreateResponse(StatusCode.BadParam);
                }

                var keys = body["keys"].AsList <object>();
                options.Keys = keys;
                return DoAllDocs(context, db, options);
            }).AsDefaultState());
        }
        /// <summary>
        /// Gets information about the specified database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/common.html#get--db
        /// </remarks>
        public static ICouchbaseResponseState GetConfiguration(ICouchbaseListenerContext context)
        {
            return PerformLogicWithDatabase(context, true, db =>
            {
                int numDocs = db.GetDocumentCount();
                long updateSequence = db.GetLastSequenceNumber();
                if (numDocs < 0 || updateSequence < 0) {
                    return context.CreateResponse(StatusCode.DbError);
                }

                var response = context.CreateResponse();
                response.JsonBody = new Body(new Dictionary<string, object> {
                    { "db_name", db.Name },
                    { "db_uuid", db.PublicUUID() },
                    { "doc_count", numDocs },
                    { "update_seq", updateSequence },
                    { "committed_update_seq", updateSequence },
                    { "purge_seq", 0 }, //TODO: Implement
                    { "disk_size", db.GetTotalDataSize() },
                    { "start_time", db.StartTime * 1000 },
                    { "revs_limit", db.GetMaxRevTreeDepth() }
                });

                return response;
            }).AsDefaultState();
        }
Exemple #4
0
        /// <summary>
        /// A database purge permanently removes the references to deleted documents from the database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/misc.html#post--db-_purge
        /// <remarks>
        public static ICouchbaseResponseState Purge(ICouchbaseListenerContext context)
        {
            return(PerformLogicWithDatabase(context, true, db =>
            {
                var body = context.BodyAs <Dictionary <string, IList <string> > >();
                if (body == null)
                {
                    return context.CreateResponse(StatusCode.BadJson);
                }

                var purgedRevisions = db.Storage.PurgeRevisions(body);
                if (purgedRevisions == null)
                {
                    return context.CreateResponse(StatusCode.DbError);
                }

                var responseBody = new Body(new Dictionary <string, object>
                {
                    { "purged", purgedRevisions }
                });

                var retVal = context.CreateResponse();
                retVal.JsonBody = responseBody;
                return retVal;
            }).AsDefaultState());
        }
Exemple #5
0
        private const int MIN_HEARTBEAT = 5000; //NOTE: iOS uses seconds but .NET uses milliseconds

        #endregion

        #region Public Methods

        /// <summary>
        /// Gets information about the specified database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/common.html#get--db
        /// <remarks>
        public static ICouchbaseResponseState GetConfiguration(ICouchbaseListenerContext context)
        {
            return(PerformLogicWithDatabase(context, true, db =>
            {
                int numDocs = db.DocumentCount;
                long updateSequence = db.LastSequenceNumber;
                if (numDocs < 0 || updateSequence < 0)
                {
                    return context.CreateResponse(StatusCode.DbError);
                }

                var response = context.CreateResponse();
                response.JsonBody = new Body(new Dictionary <string, object> {
                    { "db_name", db.Name },
                    { "doc_count", numDocs },
                    { "update_seq", updateSequence },
                    { "committed_update_seq", updateSequence },
                    { "purge_seq", 0 }, //TODO: Implement
                    { "disk_size", db.TotalDataSize },
                    { "start_time", db.StartTime * 1000 }
                });

                return response;
            }).AsDefaultState());
        }
        /// <summary>
        /// Returns the file attachment associated with the document. The raw data of the associated attachment is returned
        /// (just as if you were accessing a static file. The returned Content-Type will be the same as the content type
        /// set when the document attachment was submitted into the database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/attachments.html#get--db-docid-attname
        /// </remarks>
        public static ICouchbaseResponseState GetAttachment(ICouchbaseListenerContext context)
        {
            return(DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                Status status = new Status();
                var revID = context.GetQueryParam("rev");
                var rev = db.GetDocument(context.DocumentName, revID == null ? null : revID.AsRevID(), false, status);

                if (rev == null)
                {
                    return context.CreateResponse(status.Code);
                }
                if (context.CacheWithEtag(rev.RevID.ToString()))
                {
                    return context.CreateResponse(StatusCode.NotModified);
                }

                string acceptEncoding = context.RequestHeaders["Accept-Encoding"];
                bool acceptEncoded = acceptEncoding != null && acceptEncoding.Contains("gzip") &&
                                     context.RequestHeaders["Range"] == null;

                var attachment = db.GetAttachmentForRevision(rev, context.AttachmentName);
                if (attachment == null)
                {
                    return context.CreateResponse(StatusCode.AttachmentNotFound);
                }

                var response = context.CreateResponse();
                if (context.Method.Equals(HttpMethod.Head))
                {
                    var length = attachment.Length;
                    if (acceptEncoded && attachment.Encoding == AttachmentEncoding.GZIP &&
                        attachment.EncodedLength > 0)
                    {
                        length = attachment.EncodedLength;
                    }

                    response["Content-Length"] = length.ToString();
                }
                else
                {
                    var contents = acceptEncoded ? attachment.EncodedContent : attachment.Content;
                    if (contents == null)
                    {
                        response.InternalStatus = StatusCode.NotFound;
                        return response;
                    }

                    response.BinaryBody = contents;
                }

                response["Content-Type"] = attachment.ContentType;
                if (acceptEncoded && attachment.Encoding == AttachmentEncoding.GZIP)
                {
                    response["Content-Encoding"] = "gzip";
                }

                return response;
            }).AsDefaultState());
        }
Exemple #7
0
 /// <summary>
 /// Request compaction of the specified database. Compaction compresses the disk database file.
 /// </summary>
 /// <returns>The response state for further HTTP processing</returns>
 /// <param name="context">The context of the Couchbase Lite HTTP request</param>
 /// <remarks>
 /// http://docs.couchdb.org/en/latest/api/database/compact.html#post--db-_compact
 /// <remarks>
 public static ICouchbaseResponseState Compact(ICouchbaseListenerContext context)
 {
     return(PerformLogicWithDatabase(context, true, db =>
     {
         try {
             db.Compact();
             return context.CreateResponse(StatusCode.Accepted);
         } catch (CouchbaseLiteException) {
             return context.CreateResponse(StatusCode.DbError);
         }
     }).AsDefaultState());
 }
        /// <summary>
        /// Request, configure, or stop, a replication operation.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/server/common.html#post--_replicate
        /// <remarks>
        public static ICouchbaseResponseState ManageReplicationSession(ICouchbaseListenerContext context)
        {
            var body = default(IDictionary <string, object>);

            try {
                byte[] buffer = new byte[context.ContentLength];
                context.BodyStream.Read(buffer, 0, buffer.Length);
                body = new Body(buffer).GetProperties() ?? new Dictionary <string, object>();
            } catch (IOException e) {
                Log.To.Router.E("_replicate", "IOException while reading POST body", e);
                return(context.CreateResponse(StatusCode.RequestTimeout).AsDefaultState());
            }

            Replication rep      = context.DbManager.ReplicationWithProperties(body);
            var         response = context.CreateResponse();
            bool        cancel   = body.Get("cancel") is bool && (bool)body.Get("cancel");

            if (cancel)
            {
                if (!rep.IsRunning)
                {
                    response.InternalStatus = StatusCode.NotFound;
                }
                else
                {
                    rep.Stop();
                }
            }
            else
            {
                rep.Start();
                if (rep.Continuous || body.GetCast <bool>("async", false))
                {
                    response.JsonBody = new Body(new Dictionary <string, object> {
                        { "session_id", rep.sessionID }
                    });
                }
                else
                {
                    return(new OneShotCouchbaseResponseState(rep)
                    {
                        Response = response
                    });
                }
            }

            return(response.AsDefaultState());
        }
Exemple #9
0
        // Attempt to write the response over the wire to the client
        private static void ProcessResponse(ICouchbaseListenerContext context, ICouchbaseResponseState responseState)
        {
            CouchbaseLiteResponse responseObject = CheckForAltMethod(context, responseState.Response);

            if (!responseState.IsAsync)
            {
                try {
                    Log.To.Router.I(TAG, "Processing response for {0}...", context.RequestUrl.PathAndQuery);
                    Log.To.Router.V(TAG, "Processing request range...");
                    responseObject.ProcessRequestRanges();
                    Log.To.Router.V(TAG, "Writing headers...");
                    responseObject.WriteHeaders();
                    Log.To.Router.V(TAG, "Writing body...");
                    responseObject.WriteToContext();
                    Log.To.Router.I(TAG, "{0} {1} => {2} ({3})", context.Method, context.RequestUrl.PathAndQuery, responseObject.Status, responseObject.StatusMessage);
                } catch (Exception e) {
                    Log.To.Router.E(TAG, "Exception writing response", e);
                    responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState();
                }
            }
            else
            {
                _UnfinishedResponses.Add(responseState);
            }
        }
        /// <summary>
        /// Verifies and registers a facebook token for use in replication authentication
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        public static ICouchbaseResponseState RegisterFacebookToken(ICouchbaseListenerContext context)
        {
            var response = context.CreateResponse();
            var body = context.BodyAs<Dictionary<string, object>>();

            string email = body.GetCast<string>("email");
            string remoteUrl = body.GetCast<string>("remote_url");
            string accessToken = body.GetCast<string>("access_token");
            if (email != null && remoteUrl != null && accessToken != null) {
                Uri siteUrl;
                if (!Uri.TryCreate(remoteUrl, UriKind.Absolute, out siteUrl)) {
                    response.InternalStatus = StatusCode.BadParam;
                    response.JsonBody = new Body(new Dictionary<string, object> {
                        { "error", "invalid remote_url" }
                    });
                } else if (!FacebookAuthorizer.RegisterAccessToken(accessToken, email, siteUrl)) {
                    response.InternalStatus = StatusCode.BadParam;
                    response.JsonBody = new Body(new Dictionary<string, object> {
                        { "error", "invalid access_token" }
                    });
                } else {
                    response.JsonBody = new Body(new Dictionary<string, object> {
                        { "ok", "registered" },
                        { "email", email }
                    });
                }
            } else {
                response.InternalStatus = StatusCode.BadParam;
                response.JsonBody = new Body(new Dictionary<string, object> {
                    { "error", "required fields: access_token, email, remote_url" }
                });
            }

            return response.AsDefaultState();
        }
        /// <summary>
        /// Uploads the supplied content as an attachment to the specified document.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/attachments.html#put--db-docid-attname
        /// </remarks>
        public static ICouchbaseResponseState UpdateAttachment(ICouchbaseListenerContext context)
        {
            var state = new AsyncOpCouchbaseResponseState();

            DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                var blob     = db.AttachmentWriter;
                var httpBody = new byte[context.ContentLength];
                context.BodyStream.ReadAsync(httpBody, 0, httpBody.Length).ContinueWith(t => {
                    if (t.Result == 0)
                    {
                        state.Response = context.CreateResponse(StatusCode.BadAttachment);
                        state.SignalFinished();
                        return;
                    }

                    blob.AppendData(httpBody);
                    blob.Finish();

                    state.Response = UpdateAttachment(context, db, context.AttachmentName, context.DocumentName, blob);
                    state.SignalFinished();
                });

                return(null);
            });

            return(state);
        }
        //Do an all document request on the database (i.e. fetch all docs given some options)
        private static CouchbaseLiteResponse DoAllDocs(ICouchbaseListenerContext context, Database db, QueryOptions options)
        {
            var result = db.GetAllDocs(options);

            if (!result.ContainsKey("rows"))
            {
                return(context.CreateResponse(StatusCode.BadJson));
            }

            var documentProps = from row in (List <QueryRow>)result["rows"] select row.AsJSONDictionary();

            result["rows"] = documentProps;
            var response = context.CreateResponse();

            response.JsonBody = new Body(result);
            return(response);
        }
Exemple #13
0
        /// <summary>
        /// Returns a JSON structure of all of the documents in a given database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#get--db-_all_docs
        /// <remarks>
        public static ICouchbaseResponseState GetAllDocuments(ICouchbaseListenerContext context)
        {
            return(PerformLogicWithDatabase(context, true, db =>
            {
                if (context.CacheWithEtag(db.LastSequenceNumber.ToString()))
                {
                    return context.CreateResponse(StatusCode.NotModified);
                }

                var options = context.QueryOptions;
                if (options == null)
                {
                    return context.CreateResponse(StatusCode.BadParam);
                }

                return DoAllDocs(context, db, options);
            }).AsDefaultState());
        }
Exemple #14
0
        /// <summary>
        /// Creates a new database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/common.html#put--db
        /// <remarks>
        public static ICouchbaseResponseState UpdateConfiguration(ICouchbaseListenerContext context)
        {
            string   dbName = context.DatabaseName;
            Database db     = context.DbManager.GetDatabase(dbName, false);

            if (db != null && db.Exists())
            {
                return(context.CreateResponse(StatusCode.PreconditionFailed).AsDefaultState());
            }

            try {
                db.Open();
            } catch (CouchbaseLiteException) {
                return(context.CreateResponse(StatusCode.Exception).AsDefaultState());
            }

            return(context.CreateResponse(StatusCode.Created).AsDefaultState());
        }
Exemple #15
0
        // Check for an incorrect request method on a request
        private static CouchbaseLiteResponse CheckForAltMethod(ICouchbaseListenerContext context, CouchbaseLiteResponse response)
        {
            if (response.Status != RouteCollection.EndpointNotFoundStatus)
            {
                return(response);
            }

            var  request      = context.RequestUrl;
            bool hasAltMethod = _Delete.HasLogicForRequest(request) || _Get.HasLogicForRequest(request) ||
                                _Post.HasLogicForRequest(request) || _Put.HasLogicForRequest(request);

            if (hasAltMethod)
            {
                return(context.CreateResponse(StatusCode.MethodNotAllowed));
            }

            return(context.CreateResponse(StatusCode.NotFound));
        }
Exemple #16
0
        // Performs the actual query logic on a design document
        private static CouchbaseLiteResponse QueryDesignDocument(ICouchbaseListenerContext context, IList <object> keys)
        {
            return(DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                var view = db.GetView(String.Format("{0}/{1}", context.DesignDocName, context.ViewName));
                var status = view.CompileFromDesignDoc();
                if (status.IsError)
                {
                    return context.CreateResponse(status.Code);
                }

                var options = context.QueryOptions;
                if (options == null)
                {
                    return context.CreateResponse(StatusCode.BadRequest);
                }

                if (keys != null)
                {
                    options.Keys = keys;
                }

                if (options.Stale == IndexUpdateMode.Before || view.LastSequenceIndexed <= 0)
                {
                    view.UpdateIndex();
                }
                else if (options.Stale == IndexUpdateMode.After && view.LastSequenceIndexed < db.LastSequenceNumber)
                {
                    db.RunAsync(_ => view.UpdateIndex());
                }

                // Check for conditional GET and set response Etag header:
                if (keys == null)
                {
                    long eTag = options.IncludeDocs ? db.LastSequenceNumber : view.LastSequenceIndexed;
                    if (context.CacheWithEtag(eTag.ToString()))
                    {
                        return context.CreateResponse(StatusCode.NotModified);
                    }
                }

                return DatabaseMethods.QueryView(context, db, view, options);
            }));
        }
Exemple #17
0
        /// <summary>
        /// List of running tasks, including the task type, name, status and process ID
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/server/common.html#get--_active_tasks
        /// </remarks>
        public static ICouchbaseResponseState GetActiveTasks(ICouchbaseListenerContext context)
        {
            // Get the current task info of all replicators:
            var activity    = new List <object>();
            var replicators = new List <Replication>();

            foreach (var db in context.DbManager.AllOpenDatabases())
            {
                var activeReplicators = default(IList <Replication>);
                if (db.ActiveReplicators.AcquireTemp(out activeReplicators))
                {
                    foreach (var repl in activeReplicators)
                    {
                        replicators.Add(repl);
                        activity.Add(repl.ActiveTaskInfo);
                    }
                }
            }

            if (context.ChangesFeedMode >= ChangesFeedMode.Continuous)
            {
                // Continuous activity feed (this is a CBL-specific API):
                var response = context.CreateResponse();
                response.WriteHeaders();
                response.Chunked = true;

                foreach (var item in activity)
                {
                    response.SendContinuousLine((IDictionary <string, object>)item, context.ChangesFeedMode);
                }

                return(new ReplicationCouchbaseResponseState(replicators)
                {
                    Response = response,
                    ChangesFeedMode = context.ChangesFeedMode
                });
            }
            else
            {
                var response = context.CreateResponse();
                response.JsonBody = new Body(activity);
                return(response.AsDefaultState());
            }
        }
        /// <summary>
        /// Creates a new named document, or creates a new revision of the existing document.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/common.html#put--db-docid
        /// </remarks>
        public static ICouchbaseResponseState UpdateDocument(ICouchbaseListenerContext context)
        {
            return(PerformLogicWithDocumentBody(context, (db, body) =>
            {
                var response = context.CreateResponse();
                string docId = context.DocumentName;

                db.ForgetDesignDocument(context.DesignDocName);
                if (context.GetQueryParam <bool>("new_edits", bool.TryParse, true))
                {
                    // Regular PUT:
                    return UpdateDb(context, db, docId, body, false);
                }
                else
                {
                    // PUT with new_edits=false -- forcible insertion of existing revision:
                    RevisionInternal rev = new RevisionInternal(body);
                    if (rev == null)
                    {
                        response.InternalStatus = StatusCode.BadJson;
                        return response;
                    }

                    if (!docId.Equals(rev.DocID) || rev.RevID == null)
                    {
                        response.InternalStatus = StatusCode.BadId;
                        return response;
                    }

                    var history = Database.ParseCouchDBRevisionHistory(body.GetProperties());
                    Status status = new Status(StatusCode.Ok);
                    var castContext = context as ICouchbaseListenerContext2;
                    var source = (castContext != null && !castContext.IsLoopbackRequest) ? castContext.Sender : null;

                    try {
                        Log.To.Router.I(TAG, "Force inserting {0}", rev);
                        Log.To.Router.V(TAG, "With history {0}", new LogJsonString(history));
                        db.ForceInsert(rev, history, source);
                    } catch (CouchbaseLiteException e) {
                        status = e.CBLStatus;
                    }

                    if (!status.IsError)
                    {
                        response.JsonBody = new Body(new Dictionary <string, object> {
                            { "ok", true },
                            { "id", rev.DocID },
                            { "rev", rev.RevID }
                        });
                    }

                    response.InternalStatus = status.Code;
                    return response;
                }
            }).AsDefaultState());
        }
        /// <summary>
        /// Returns a list of all the databases in the Couchbase Lite instance.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/server/common.html#get--_all_dbs
        /// <remarks>
        public static ICouchbaseResponseState GetAllDbs(ICouchbaseListenerContext context)
        {
            var names = context.DbManager.AllDatabaseNames.Cast <object>().ToList();
            var body  = new Body(names);

            var couchResponse = context.CreateResponse();

            couchResponse.JsonBody = body;
            return(couchResponse.AsDefaultState());
        }
Exemple #20
0
        /// <summary>
        /// Deletes the specified database, and all the documents and attachments contained within it.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/common.html#delete--db
        /// <remarks>
        public static ICouchbaseResponseState DeleteConfiguration(ICouchbaseListenerContext context)
        {
            return(PerformLogicWithDatabase(context, false, db =>
            {
                if (context.GetQueryParam("rev") != null)
                {
                    // CouchDB checks for this; probably meant to be a document deletion
                    return context.CreateResponse(StatusCode.BadId);
                }

                try {
                    db.Delete();
                } catch (CouchbaseLiteException) {
                    return context.CreateResponse(StatusCode.InternalServerError);
                }

                return context.CreateResponse();
            }).AsDefaultState());
        }
Exemple #21
0
        //Do an all document request on the database (i.e. fetch all docs given some options)
        private static CouchbaseLiteResponse DoAllDocs(ICouchbaseListenerContext context, Database db, QueryOptions options)
        {
            var iterator = db.GetAllDocs(options);

            if (iterator == null)
            {
                return(context.CreateResponse(StatusCode.BadJson));
            }

            var response = context.CreateResponse();
            var result   = (from row in iterator
                            select row.AsJSONDictionary()).ToList();

            response.JsonBody = new Body(new NonNullDictionary <string, object> {
                { "rows", result },
                { "total_rows", result.Count },
                { "offset", options.Skip },
                { "update_seq", options.UpdateSeq ? (object)db.LastSequenceNumber : null }
            });
            return(response);
        }
        /// <summary>
        /// Requests one or more Universally Unique Identifiers (UUIDs) from the Couchbase Lite instance.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/server/common.html#get--_uuids
        /// <remarks>
        public static ICouchbaseResponseState GetUUIDs(ICouchbaseListenerContext context)
        {
            int count = context.GetQueryParam <int>("count", int.TryParse, 1);

            if (count > 1000)
            {
                return(context.CreateResponse(StatusCode.Forbidden).AsDefaultState());
            }

            var uuidList = new List <object>();

            for (int i = 0; i < count; i++)
            {
                uuidList.Add(Guid.NewGuid());
            }

            var couchResponse = context.CreateResponse();

            couchResponse.JsonBody = new Body(uuidList);
            return(couchResponse.AsDefaultState());
        }
Exemple #23
0
        /// <summary>
        /// Performs the given logic with the specified database
        /// </summary>
        /// <returns>The result (in terms of response to the client) of the database operation</returns>
        /// <param name="context">The Couchbase Lite HTTP context</param>
        /// <param name="open">Whether or not to open the database, or just find it</param>
        /// <param name="action">The logic to perform on the database</param>
        public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open,
                                                                     Func <Database, CouchbaseLiteResponse> action)
        {
            string   dbName = context.DatabaseName;
            Database db     = context.DbManager.GetDatabase(dbName, false);

            if (db == null || !db.Exists())
            {
                return(context.CreateResponse(StatusCode.NotFound));
            }

            if (open)
            {
                try {
                    db.Open();
                } catch (Exception) {
                    return(context.CreateResponse(StatusCode.DbError));
                }
            }

            return(action(db));
        }
Exemple #24
0
        // Check for an incorrect request method on a request
        private static CouchbaseLiteResponse CheckForAltMethod(ICouchbaseListenerContext context, CouchbaseLiteResponse response)
        {
            if (response.Status != RouteCollection.EndpointNotFoundStatus)
            {
                return(response);
            }

            Log.To.Router.I(TAG, "{0} method not found for endpoint {1}, searching for alternate...",
                            context.Method, context.RequestUrl.PathAndQuery);
            var  request      = context.RequestUrl;
            bool hasAltMethod = _Delete.HasLogicForRequest(request) || _Get.HasLogicForRequest(request) ||
                                _Post.HasLogicForRequest(request) || _Put.HasLogicForRequest(request);

            if (hasAltMethod)
            {
                Log.To.Router.I(TAG, "Suitable method found; returning 406");
                return(context.CreateResponse(StatusCode.MethodNotAllowed));
            }

            Log.To.Router.I(TAG, "No suitable method found; returning 404", context.RequestUrl.PathAndQuery);
            return(context.CreateResponse(StatusCode.NotFound));
        }
        // Factors out the logic of opening the database and reading the document body from the HTTP request
        // and performs the specified logic on the body received in the request, barring any problems
        private static CouchbaseLiteResponse PerformLogicWithDocumentBody(ICouchbaseListenerContext context,
                                                                          Func <Database, Body, CouchbaseLiteResponse> callback)
        {
            return(DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                MultipartDocumentReader reader = new MultipartDocumentReader(db);
                reader.SetContentType(context.RequestHeaders["Content-Type"]);

                try {
                    reader.AppendData(context.BodyStream.ReadAllBytes());
                    reader.Finish();
                } catch (InvalidOperationException e) {
                    Log.To.Router.E(TAG, "Exception trying to read data from multipart upload", e);
                    return context.CreateResponse(StatusCode.BadRequest);
                } catch (IOException e) {
                    Log.To.Router.E(TAG, "IOException while reading context body", e);
                    return context.CreateResponse(StatusCode.RequestTimeout);
                }

                return callback(db, new Body(reader.GetDocumentProperties()));
            }));
        }
Exemple #26
0
        /// <summary>
        /// Creates a new named document, or creates a new revision of the existing document.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/common.html#put--db-docid
        /// <remarks>
        public static ICouchbaseResponseState UpdateDocument(ICouchbaseListenerContext context)
        {
            return(PerformLogicWithDocumentBody(context, (db, body) =>
            {
                var response = context.CreateResponse();
                string docId = context.DocumentName;
                if (context.GetQueryParam <bool>("new_edits", bool.TryParse, true))
                {
                    // Regular PUT:
                    return UpdateDb(context, db, docId, body, false);
                }
                else
                {
                    // PUT with new_edits=false -- forcible insertion of existing revision:
                    RevisionInternal rev = new RevisionInternal(body);
                    if (rev == null)
                    {
                        response.InternalStatus = StatusCode.BadJson;
                        return response;
                    }

                    if (!docId.Equals(rev.GetDocId()) || rev.GetRevId() == null)
                    {
                        response.InternalStatus = StatusCode.BadId;
                        return response;
                    }

                    var history = Database.ParseCouchDBRevisionHistory(body.GetProperties());
                    Status status = new Status();
                    try {
                        db.ForceInsert(rev, history, null, status);
                    } catch (CouchbaseLiteException e) {
                        status = e.GetCBLStatus();
                    }

                    if (!status.IsError)
                    {
                        response.JsonBody = new Body(new Dictionary <string, object> {
                            { "ok", true },
                            { "id", rev.GetDocId() },
                            { "rev", rev.GetRevId() }
                        });
                    }

                    response.InternalStatus = status.GetCode();
                    return response;
                }
            }).AsDefaultState());
        }
        // Performs the actual query logic on a design document
        private static CouchbaseLiteResponse QueryDesignDocument(ICouchbaseListenerContext context, IList<object> keys)
        {
            return DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                var view = db.GetView(String.Format("{0}/{1}", context.DesignDocName, context.ViewName));
                var status = view.CompileFromDesignDoc();
                if(status.IsError) {
                    return context.CreateResponse(status.Code);
                }

                var options = context.QueryOptions;
                if(options == null) {
                    return context.CreateResponse(StatusCode.BadRequest);
                }

                if(keys != null) {
                    options.Keys = keys;
                }

                if(options.Stale == IndexUpdateMode.Before || view.LastSequenceIndexed <= 0) {
                    view.UpdateIndex();
                } else if(options.Stale == IndexUpdateMode.After && view.LastSequenceIndexed < db.LastSequenceNumber) {
                    db.RunAsync(_ => view.UpdateIndex());
                }

                // Check for conditional GET and set response Etag header:
                if(keys == null) {
                    long eTag = options.IncludeDocs ? db.LastSequenceNumber : view.LastSequenceIndexed;
                    if(context.CacheWithEtag(eTag.ToString())) {
                        return context.CreateResponse(StatusCode.NotModified);
                    }
                }

                return DatabaseMethods.QueryView(context, db, view, options);
            });
        }
        /// <summary>
        /// Returns complete information about authenticated user (stubbed, not actual functionality)
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/server/authn.html#get--_session
        /// <remarks>
        public static ICouchbaseResponseState GetSession(ICouchbaseListenerContext context)
        {
            // Even though CouchbaseLite doesn't support user logins, it implements a generic response to the
            // CouchDB _session API, so that apps that call it (such as Futon!) won't barf.
            var couchResponse = context.CreateResponse();

            couchResponse.JsonBody = new Body(new Dictionary <string, object> {
                { "ok", true },
                { "userCtx", new Dictionary <string, object> {
                      { "name", null },
                      { "roles", new[] { "_admin" } }
                  } }
            });

            return(couchResponse.AsDefaultState());
        }
Exemple #29
0
        // Factors out the logic of opening the database and reading the document body from the HTTP request
        // and performs the specified logic on the body received in the request, barring any problems
        private static CouchbaseLiteResponse PerformLogicWithDocumentBody(ICouchbaseListenerContext context,
                                                                          Func <Database, Body, CouchbaseLiteResponse> callback)
        {
            return(DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                MultipartDocumentReader reader = new MultipartDocumentReader(db);
                reader.SetContentType(context.RequestHeaders["Content-Type"]);
                reader.AppendData(context.BodyStream.ReadAllBytes());
                try {
                    reader.Finish();
                } catch (InvalidOperationException) {
                    return context.CreateResponse(StatusCode.BadRequest);
                }

                return callback(db, new Body(reader.GetDocumentProperties()));
            }));
        }
        /// <summary>
        /// Returns a JSON structure containing information about the server, including a welcome message and the version of the server.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/server/common.html#get--
        /// <remarks>
        public static ICouchbaseResponseState Greeting(ICouchbaseListenerContext context)
        {
            var info = new Dictionary<string, object> {
                { "couchdb", "Welcome" }, //for compatibility
                { "CouchbaseLite", "Welcome" },
                { "version", Manager.VersionString },
                { "vendor", new Dictionary<string, object> {
                        { "name", "Couchbase Lite (C#)" },
                        { "version", Manager.VersionString }
                    }
                }
            };

            var body = new Body(info);
            var couchResponse = context.CreateResponse();
            couchResponse.JsonBody = body;
            return couchResponse.AsDefaultState();
        }
        /// <summary>
        /// Returns a JSON structure containing information about the server, including a welcome message and the version of the server.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/server/common.html#get--
        /// <remarks>
        public static ICouchbaseResponseState Greeting(ICouchbaseListenerContext context)
        {
            var info = new Dictionary <string, object> {
                { "couchdb", "Welcome" }, //for compatibility
                { "CouchbaseLite", "Welcome" },
                { "version", Manager.VersionString },
                { "vendor", new Dictionary <string, object> {
                      { "name", "Couchbase Lite (C#)" },
                      { "version", Manager.VersionString }
                  } }
            };

            var body          = new Body(info);
            var couchResponse = context.CreateResponse();

            couchResponse.JsonBody = body;
            return(couchResponse.AsDefaultState());
        }
Exemple #32
0
        /// <summary>
        /// Verifies and registers a facebook token for use in replication authentication
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        public static ICouchbaseResponseState RegisterFacebookToken(ICouchbaseListenerContext context)
        {
            var response = context.CreateResponse();
            var body     = context.BodyAs <Dictionary <string, object> >();

            string email       = body.GetCast <string>("email");
            string remoteUrl   = body.GetCast <string>("remote_url");
            string accessToken = body.GetCast <string>("access_token");

            if (email != null && remoteUrl != null && accessToken != null)
            {
                Uri siteUrl;
                if (!Uri.TryCreate(remoteUrl, UriKind.Absolute, out siteUrl))
                {
                    response.InternalStatus = StatusCode.BadParam;
                    response.JsonBody       = new Body(new Dictionary <string, object> {
                        { "error", "invalid remote_url" }
                    });
                }
                else if (!FacebookAuthorizer.RegisterAccessToken(accessToken, email, siteUrl))
                {
                    response.InternalStatus = StatusCode.BadParam;
                    response.JsonBody       = new Body(new Dictionary <string, object> {
                        { "error", "invalid access_token" }
                    });
                }
                else
                {
                    response.JsonBody = new Body(new Dictionary <string, object> {
                        { "ok", "registered" },
                        { "email", email }
                    });
                }
            }
            else
            {
                response.InternalStatus = StatusCode.BadParam;
                response.JsonBody       = new Body(new Dictionary <string, object> {
                    { "error", "required fields: access_token, email, remote_url" }
                });
            }

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

            object updateSeq    = options.UpdateSeq ? (object)view.LastSequenceIndexed : null;
            var    mappedResult = new List <object>();

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

                mappedResult.Add(dict);
            }

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

            var retVal = context.CreateResponse();

            retVal.JsonBody = body;
            return(retVal);
        }
        /// <summary>
        /// Verifies and registers a persona token for use in replication authentication
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        public static ICouchbaseResponseState RegisterPersonaToken(ICouchbaseListenerContext context)
        {
            var response = context.CreateResponse();
            var body = context.BodyAs<Dictionary<string, object>>();

            string email = PersonaAuthorizer.RegisterAssertion(body.GetCast<string>("assertion"));
            if (email != null) {
                response.JsonBody = new Body(new Dictionary<string, object> {
                    { "ok", "registered" },
                    { "email", email }
                });
            } else {
                response.InternalStatus = StatusCode.BadParam;
                response.JsonBody = new Body(new Dictionary<string, object> {
                    { "error", "invalid assertion" }
                });
            }

            return response.AsDefaultState();
        }
Exemple #35
0
        // Attempt to write the response over the wire to the client
        private static void ProcessResponse(ICouchbaseListenerContext context, ICouchbaseResponseState responseState)
        {
            CouchbaseLiteResponse responseObject = CheckForAltMethod(context, responseState.Response);

            if (!responseState.IsAsync)
            {
                try {
                    responseObject.ProcessRequestRanges();
                    responseObject.WriteHeaders();
                    responseObject.WriteToContext();
                } catch (Exception e) {
                    Log.E(TAG, "Exception writing response", e);
                    responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState();
                }
            }
            else
            {
                _UnfinishedResponses.Add(responseState);
            }
        }
 /// <summary>
 /// Request compaction of the specified database. Compaction compresses the disk database file.
 /// </summary>
 /// <returns>The response state for further HTTP processing</returns>
 /// <param name="context">The context of the Couchbase Lite HTTP request</param>
 /// <remarks>
 /// http://docs.couchdb.org/en/latest/api/database/compact.html#post--db-_compact
 /// </remarks>
 public static ICouchbaseResponseState Compact(ICouchbaseListenerContext context)
 {
     return PerformLogicWithDatabase(context, true, db =>
     {
         try {
             db.Compact();
             return context.CreateResponse(StatusCode.Accepted);
         } catch (CouchbaseLiteException) {
             return context.CreateResponse(StatusCode.DbError);
         }
     }).AsDefaultState();
 }
        public static ICouchbaseResponseState GetChangesPost(ICouchbaseListenerContext context)
        {
            DBMonitorCouchbaseResponseState responseState = new DBMonitorCouchbaseResponseState();

            var responseObject = PerformLogicWithDatabase(context, true, db =>
            {
                var response = context.CreateResponse();
                responseState.Response = response;
                var body = context.BodyAs<Dictionary<string, object>>();
                ProcessBody(body);
                if (body.GetCast<ChangesFeedMode>("feed") < ChangesFeedMode.Continuous) {
                    if(context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) {
                        response.InternalStatus = StatusCode.NotModified;
                        return response;
                    }
                }

                var options = ChangesOptions.Default;
                responseState.Db = db;
                responseState.ContentOptions = body.GetCast<DocumentContentOptions>("content_options");
                responseState.ChangesFeedMode = body.GetCast<ChangesFeedMode>("feed");
                responseState.ChangesIncludeDocs = body.GetCast<bool>("include_docs");
                options.IncludeDocs = responseState.ChangesIncludeDocs;
                responseState.ChangesIncludeConflicts = body.GetCast<string>("style") == "all_docs";
                options.IncludeConflicts = responseState.ChangesIncludeConflicts;
                options.ContentOptions = responseState.ContentOptions;
                options.SortBySequence = !options.IncludeConflicts;
                options.Limit = body.GetCast<int>("limit", options.Limit);
                int since = body.GetCast<int>("since");

                string filterName = body.GetCast<string>("filter");
                if(filterName != null) {
                    Status status = new Status();
                    responseState.ChangesFilter = db.GetFilter(filterName, status);
                    if(responseState.ChangesFilter == null) {
                        return context.CreateResponse(status.Code);
                    }

                    responseState.FilterParams = context.GetQueryParams();
                }


                RevisionList changes = db.ChangesSince(since, options, responseState.ChangesFilter, responseState.FilterParams);
                if((responseState.ChangesFeedMode >= ChangesFeedMode.Continuous) || 
                    (responseState.ChangesFeedMode == ChangesFeedMode.LongPoll && changes.Count == 0)) {
                    // Response is going to stay open (continuous, or hanging GET):
                    response.Chunked = true;
                    if(responseState.ChangesFeedMode == ChangesFeedMode.EventSource) {
                        response["Content-Type"] = "text/event-stream; charset=utf-8";
                    }

                    if(responseState.ChangesFeedMode >= ChangesFeedMode.Continuous) {
                        response.WriteHeaders();
                        foreach(var rev in changes) {
                            response.SendContinuousLine(ChangesDictForRev(rev, responseState), context.ChangesFeedMode);
                        }
                    }

                    responseState.SubscribeToDatabase(db);
                    int heartbeat = body.GetCast<int>("heartbeat", Int32.MinValue);
                    if(heartbeat != Int32.MinValue) {
                        if(heartbeat <= 0) {
                            responseState.IsAsync = false;
                            return context.CreateResponse(StatusCode.BadParam);
                        }

                        heartbeat = Math.Max(heartbeat, (int)MinHeartbeat.TotalMilliseconds);
                        string heartbeatResponse = context.ChangesFeedMode == ChangesFeedMode.EventSource ? "\n\n" : "\r\n";
                        responseState.StartHeartbeat(heartbeatResponse, TimeSpan.FromMilliseconds(heartbeat));
                    }

                    return context.CreateResponse();
                } else {
                    if(responseState.ChangesIncludeConflicts) {
                        response.JsonBody = new Body(ResponseBodyForChanges(changes, since, options.Limit, responseState));
                    } else {
                        response.JsonBody = new Body(ResponseBodyForChanges(changes, since, responseState));
                    }

                    return response;
                }
            });

            responseState.Response = responseObject;
            return responseState;
        }
        /// <summary>
        /// Returns a sorted list of changes made to documents in the database, in time order of application.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/changes.html#get--db-_changes
        /// </remarks>
        public static ICouchbaseResponseState GetChanges(ICouchbaseListenerContext context)
        {
            DBMonitorCouchbaseResponseState responseState = new DBMonitorCouchbaseResponseState();

            var responseObject = PerformLogicWithDatabase(context, true, db =>
            {
                var response = context.CreateResponse();
                responseState.Response = response;
                if (context.ChangesFeedMode < ChangesFeedMode.Continuous) {
                    if(context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) {
                        response.InternalStatus = StatusCode.NotModified;
                        return response;
                    }
                }
                    
                var options = ChangesOptions.Default;
                responseState.Db = db;
                responseState.ContentOptions = context.ContentOptions;
                responseState.ChangesFeedMode = context.ChangesFeedMode;
                responseState.ChangesIncludeDocs = context.GetQueryParam<bool>("include_docs", bool.TryParse, false);
                options.IncludeDocs = responseState.ChangesIncludeDocs;
                responseState.ChangesIncludeConflicts = context.GetQueryParam("style") == "all_docs";
                options.IncludeConflicts = responseState.ChangesIncludeConflicts;
                options.ContentOptions = context.ContentOptions;
                options.SortBySequence = !options.IncludeConflicts;
                options.Limit = context.GetQueryParam<int>("limit", int.TryParse, options.Limit);
                int since = context.GetQueryParam<int>("since", int.TryParse, 0);

                string filterName = context.GetQueryParam("filter");
                if(filterName != null) {
                    Status status = new Status();
                    responseState.ChangesFilter = db.GetFilter(filterName, status);
                    if(responseState.ChangesFilter == null) {
                        return context.CreateResponse(status.Code);
                    }

                    responseState.FilterParams = context.GetQueryParams();
                }


                RevisionList changes = db.ChangesSince(since, options, responseState.ChangesFilter, responseState.FilterParams);
                if((context.ChangesFeedMode >= ChangesFeedMode.Continuous) || 
                    (context.ChangesFeedMode == ChangesFeedMode.LongPoll && changes.Count == 0)) {
                    // Response is going to stay open (continuous, or hanging GET):
                    response.Chunked = true;
                    if(context.ChangesFeedMode == ChangesFeedMode.EventSource) {
                        response["Content-Type"] = "text/event-stream; charset=utf-8";
                    }

                    if(context.ChangesFeedMode >= ChangesFeedMode.Continuous) {
                        response.WriteHeaders();
                        foreach(var rev in changes) {
                            var success = response.SendContinuousLine(ChangesDictForRev(rev, responseState), context.ChangesFeedMode);
                            if(!success) {
                                return context.CreateResponse(StatusCode.BadRequest);
                            }
                        }
                    }

                    responseState.SubscribeToDatabase(db);
                    string heartbeatParam = context.GetQueryParam("heartbeat");
                    if(heartbeatParam != null) {
                        int heartbeat;
                        if(!int.TryParse(heartbeatParam, out heartbeat) || heartbeat <= 0) {
                            responseState.IsAsync = false;
                            return context.CreateResponse(StatusCode.BadParam);
                        }

                        var heartbeatSpan = TimeSpan.FromMilliseconds(heartbeat);
                        if(heartbeatSpan < MinHeartbeat) {
                            heartbeatSpan = MinHeartbeat;
                        }

                        string heartbeatResponse = context.ChangesFeedMode == ChangesFeedMode.EventSource ? "\n\n" : "\r\n";
                        responseState.StartHeartbeat(heartbeatResponse, heartbeatSpan);
                    }

                    return response;
                } else {
                    if(responseState.ChangesIncludeConflicts) {
                        response.JsonBody = new Body(ResponseBodyForChanges(changes, since, options.Limit, responseState));
                    } else {
                        response.JsonBody = new Body(ResponseBodyForChanges(changes, since, responseState));
                    }

                    return response;
                }
            });

            responseState.Response = responseObject;
            return responseState;
        }
        /// <summary>
        /// Create and update multiple documents at the same time within a single request.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_bulk_docs
        /// </remarks>
        public static ICouchbaseResponseState ProcessDocumentChangeOperations(ICouchbaseListenerContext context)
        {
            return PerformLogicWithDatabase(context, true, db =>
            {
                var postBody = context.BodyAs<Dictionary<string, object>>();
                if(postBody == null) {
                    return context.CreateResponse(StatusCode.BadJson);
                }
                    
                if(!postBody.ContainsKey("docs")) {
                    return context.CreateResponse(StatusCode.BadParam);
                }
                var docs = postBody["docs"].AsList<IDictionary<string, object>>();

                bool allOrNothing;
                postBody.TryGetValue<bool>("all_or_nothing", out allOrNothing);

                bool newEdits;
                postBody.TryGetValue<bool>("new_edits", out newEdits);

                var response = context.CreateResponse();
                StatusCode status = StatusCode.Ok;
                bool success = db.RunInTransaction(() => {
                    List<IDictionary<string, object>> results = new List<IDictionary<string, object>>(docs.Count);
                    var castContext = context as ICouchbaseListenerContext2;
                    var source = castContext != null && !castContext.IsLoopbackRequest ? castContext.Sender : null;
                    foreach(var doc in docs) {
                        string docId = doc.CblID();
                        RevisionInternal rev = null;
                        Body body = new Body(doc);

                        if(!newEdits) {
                            if(!RevisionInternal.IsValid(body)) {
                                status = StatusCode.BadParam;
                            } else {
                                rev = new RevisionInternal(body);
                                var history = Database.ParseCouchDBRevisionHistory(doc);
                                try {
                                    db.ForceInsert(rev, history, source);
                                } catch(CouchbaseLiteException e) {
                                    status = e.Code;
                                }
                            } 
                        } else {
                            status = DocumentMethods.UpdateDocument(context, db, docId, body, false, allOrNothing, out rev);
                        }

                        IDictionary<string, object> result = null;
                        if((int)status < 300) {
                            Debug.Assert(rev != null && rev.RevID != null);
                            if(newEdits) {
                                result = new Dictionary<string, object>
                                {
                                    { "id", rev.DocID },
                                    { "rev", rev.RevID },
                                    { "status", (int)status }
                                };
                            }
                        } else if((int)status >= 500) {
                            return false; // abort the whole thing if something goes badly wrong
                        } else if(allOrNothing) {
                            return false; // all_or_nothing backs out if there's any error
                        } else {
                            var info = Status.ToHttpStatus(status);
                            result = new Dictionary<string, object>
                            {
                                { "id", docId },
                                { "error", info.Item2 },
                                { "status", info.Item1 }
                            };
                        }

                        if(result != null) {
                            results.Add(result);
                        }
                    }

                    response.JsonBody = new Body(results.Cast<object>().ToList());
                    return true;
                });

                if(!success) {
                    response.InternalStatus = status;
                }

                return response;
            }).AsDefaultState();
        }
        /// <summary>
        /// The POST to _all_docs allows to specify multiple keys to be selected from the database. 
        /// This enables you to request multiple documents in a single request, in place of multiple GET /{db}/{docid} requests.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#post--db-_all_docs
        /// </remarks>
        public static ICouchbaseResponseState GetAllSpecifiedDocuments(ICouchbaseListenerContext context)
        {
            return PerformLogicWithDatabase(context, true, db =>
            {
                var options = context.QueryOptions;
                if(options == null) {
                    return context.CreateResponse(StatusCode.BadParam);
                }
                    
                var body = context.BodyAs<Dictionary<string, object>>();
                if(body == null) {
                    return context.CreateResponse(StatusCode.BadJson);
                }

                if(!body.ContainsKey("keys")) {
                    return context.CreateResponse(StatusCode.BadParam);
                }

                var keys = body["keys"].AsList<object>();
                options.Keys = keys;
                return DoAllDocs(context, db, options);
            }).AsDefaultState();
        }
        // Perform a document operation on the specified database
        private static CouchbaseLiteResponse UpdateDb(ICouchbaseListenerContext context, Database db, string docId, Body body, bool deleting)
        {
            var response = context.CreateResponse();
            if (docId != null) {
                // On PUT/DELETE, get revision ID from either ?rev= query, If-Match: header, or doc body:
                string revParam = context.GetQueryParam("rev");
                string ifMatch = context.RequestHeaders["If-Match"];
                if (ifMatch != null) {
                    if (revParam == null) {
                        revParam = ifMatch;
                    } else if (!revParam.Equals(ifMatch)) {
                        return context.CreateResponse(StatusCode.BadRequest);
                    }
                }

                if (revParam != null && body != null) {
                    var revProp = body.GetPropertyForKey("_rev");
                    if (revProp == null) {
                        // No _rev property in body, so use ?rev= query param instead:
                        var props = body.GetProperties();
                        props["_rev"] = revParam;
                        body = new Body(props);
                    } else if (!revProp.Equals(revParam)) {
                        return context.CreateResponse(StatusCode.BadRequest); // mismatch between _rev and rev
                    }
                }
            }

            RevisionInternal rev;
            StatusCode status = UpdateDocument(context, db, docId, body, deleting, false, out rev);
            if ((int)status < 300) {
                context.CacheWithEtag(rev.GetRevId()); // set ETag
                if (!deleting) {
                    var url = context.RequestUrl;
                    if (docId != null) {
                        response["Location"] = url.AbsoluteUri;
                    }
                }

                response.JsonBody = new Body(new Dictionary<string, object> {
                    { "ok", true },
                    { "id", rev.GetDocId() },
                    { "rev", rev.GetRevId() }
                });
            }

            response.InternalStatus = status;
            return response;
        }
 //Do an all document request on the database (i.e. fetch all docs given some options)
 private static CouchbaseLiteResponse DoAllDocs(ICouchbaseListenerContext context, Database db, QueryOptions options)
 {
     var iterator = db.GetAllDocs(options);
     if (iterator == null) {
         return context.CreateResponse(StatusCode.BadJson);
     }
         
     var response = context.CreateResponse();
     var result = (from row in iterator
         select row.AsJSONDictionary()).ToList();
     response.JsonBody = new Body(new NonNullDictionary<string, object> {
         { "rows", result },
         { "total_rows", result.Count },
         { "offset", options.Skip },
         { "update_seq", options.UpdateSeq ? (object)db.GetLastSequenceNumber() : null }
     });
     return response;
 }
        /// <summary>
        /// Queries the specified view using the specified options
        /// </summary>
        /// <returns>The HTTP response containing the results of the query</returns>
        /// <param name="context">The request context</param>
        /// <param name="db">The database to run the query in</param>
        /// <param name="view">The view to query</param>
        /// <param name="options">The options to apply to the query</param>
        public static CouchbaseLiteResponse QueryView(ICouchbaseListenerContext context, Database db, View view, QueryOptions options)
        {
            var result = view.QueryWithOptions(options);

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

                mappedResult.Add(dict);
            }

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

            var retVal = context.CreateResponse();
            retVal.JsonBody = body;
            return retVal;
        }
 // Attempt to write the response over the wire to the client
 private static void ProcessResponse(ICouchbaseListenerContext context, ICouchbaseResponseState responseState)
 {
     CouchbaseLiteResponse responseObject = CheckForAltMethod(context, responseState.Response);
     if (!responseState.IsAsync) {
         try {
             responseObject.ProcessRequestRanges();
             responseObject.WriteHeaders();
             responseObject.WriteToContext();
         } catch(Exception e) {
             Log.E(TAG, "Exception writing response", e);
             responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState();
         }
     } else {
         _UnfinishedResponses.Add(responseState);
     }
 }
        /// <summary>
        /// A database purge permanently removes the references to deleted documents from the database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/misc.html#post--db-_purge
        /// </remarks>
        public static ICouchbaseResponseState Purge(ICouchbaseListenerContext context)
        {
            return PerformLogicWithDatabase(context, true, db =>
            {
                var body = context.BodyAs<Dictionary<string, IList<string>>>();
                if(body == null) {
                    return context.CreateResponse(StatusCode.BadJson);
                }

                var purgedRevisions = db.Storage.PurgeRevisions(body);
                if(purgedRevisions == null) {
                    return context.CreateResponse(StatusCode.DbError);
                }

                var responseBody = new Body(new Dictionary<string, object>
                {
                    { "purged", purgedRevisions }
                });

                var retVal = context.CreateResponse();
                retVal.JsonBody = responseBody;
                return retVal;
            }).AsDefaultState();
        }
        /// <summary>
        /// Returns document by the specified docid from the specified db. Unless you request a 
        /// specific revision, the latest revision of the document will always be returned.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid
        /// <remarks>
        public static ICouchbaseResponseState GetDocument(ICouchbaseListenerContext context)
        {
            return DatabaseMethods.PerformLogicWithDatabase(context, true, db => {
                var response = context.CreateResponse();
                string docId = context.DocumentName;
                bool isLocalDoc = docId.StartsWith("_local");

                DocumentContentOptions options = context.ContentOptions;
                string openRevsParam = context.GetQueryParam("open_revs");
                bool mustSendJson = context.ExplicitlyAcceptsType("application/json");
                if (openRevsParam == null || isLocalDoc) {
                    //Regular GET:
                    string revId = context.GetQueryParam("rev"); //often null
                    RevisionInternal rev;
                    bool includeAttachments = false, sendMultipart = false;
                    if (isLocalDoc) {
                        rev = db.Storage.GetLocalDocument(docId, revId);
                    } else {
                        includeAttachments = options.HasFlag(DocumentContentOptions.IncludeAttachments);
                        if(includeAttachments) {
                            sendMultipart = !mustSendJson;
                            options &= ~DocumentContentOptions.IncludeAttachments;
                        }

                        Status status = new Status();
                        rev = db.GetDocument(docId, revId, true, status);
                        if(rev != null) {
                            rev = ApplyOptions(options, rev, context, db, status);
                        }

                        if(rev == null) {
                            if(status.Code == StatusCode.Deleted) {
                                response.StatusReason = "deleted";
                            } else {
                                response.StatusReason = "missing";
                            }

                            response.InternalStatus = status.Code;
                            return response;
                        }
                    }

                    if(rev == null) {
                        response.InternalStatus = StatusCode.NotFound;
                        return response;
                    }

                    if(context.CacheWithEtag(rev.GetRevId())) {
                        response.InternalStatus = StatusCode.NotModified;
                        return response;
                    }

                    if(!isLocalDoc && includeAttachments) {
                        int minRevPos = 1;
                        IList<string> attsSince = context.GetJsonQueryParam("atts_since").AsList<string>();
                        string ancestorId = db.Storage.FindCommonAncestor(rev, attsSince);
                        if(ancestorId != null) {
                            minRevPos = RevisionInternal.GenerationFromRevID(ancestorId) + 1;
                        }

                        Status status = new Status();
                        bool attEncodingInfo = context.GetQueryParam<bool>("att_encoding_info", bool.TryParse, false);
                        if(!db.ExpandAttachments(rev, minRevPos, sendMultipart, attEncodingInfo, status)) {
                            response.InternalStatus = status.Code;
                            return response;
                        }
                    }

                    if(sendMultipart) {
                        response.MultipartWriter = db.MultipartWriterForRev(rev, "multipart/related");
                    } else {
                        response.JsonBody = rev.GetBody();
                    }
                } else {
                    // open_revs query:
                    IList<IDictionary<string, object>> result;
                    if(openRevsParam.Equals("all")) {
                        // ?open_revs=all returns all current/leaf revisions:
                        bool includeDeleted = context.GetQueryParam<bool>("include_deleted", bool.TryParse, false);
                        RevisionList allRevs = db.Storage.GetAllDocumentRevisions(docId, true);

                        result = new List<IDictionary<string, object>>();
                        foreach(var rev in allRevs) {
                            if(!includeDeleted && rev.IsDeleted()) {
                                continue;
                            }

                            Status status = new Status();
                            RevisionInternal loadedRev = db.RevisionByLoadingBody(rev, status);
                            if(loadedRev != null) {
                                ApplyOptions(options, loadedRev, context, db, status);
                            }

                            if(loadedRev != null) {
                                result.Add(new Dictionary<string, object> { { "ok", loadedRev.GetProperties() } });
                            } else if(status.Code <= StatusCode.InternalServerError) {
                                result.Add(new Dictionary<string, object> { { "missing", rev.GetRevId() } });
                            } else {
                                response.InternalStatus = status.Code;
                                return response;
                            }
                        }
                    } else {
                        // ?open_revs=[...] returns an array of specific revisions of the document:
                        var openRevs = context.GetJsonQueryParam("open_revs").AsList<object>();
                        if(openRevs == null) {
                            response.InternalStatus = StatusCode.BadParam;
                            return response;
                        }

                        result = new List<IDictionary<string, object>>();
                        foreach(var revIDObj in openRevs) {
                            var revID = revIDObj as string;
                            if(revID == null) {
                                response.InternalStatus = StatusCode.BadId;
                                return response;
                            }

                            Status status = new Status();
                            var rev = db.GetDocument(docId, revID, true, status);
                            if(rev != null) {
                                rev = ApplyOptions(options, rev, context, db, status);
                            }

                            if(rev != null) {
                                result.Add(new Dictionary<string, object>{ { "ok", rev.GetProperties() } });
                            } else {
                                result.Add(new Dictionary<string, object>{ { "missing", revID } });
                            }
                        }
                    }

                    if(mustSendJson) {
                        response["Content-Type"] = "application/json";
                        response.JsonBody = new Body(result.Cast<object>().ToList());
                    } else {
                        response.SetMultipartBody(result.Cast<object>().ToList(), "multipart/mixed");
                    }
                }

                return response;
            }).AsDefaultState();
        }
        // Check for an incorrect request method on a request
        private static CouchbaseLiteResponse CheckForAltMethod(ICouchbaseListenerContext context, CouchbaseLiteResponse response)
        {
            if (response.Status != RouteCollection.EndpointNotFoundStatus) {
                return response;
            }
                
            var request = context.RequestUrl;
            bool hasAltMethod = _Delete.HasLogicForRequest(request) || _Get.HasLogicForRequest(request)
                                || _Post.HasLogicForRequest(request) || _Put.HasLogicForRequest(request);

            if (hasAltMethod) {
                return context.CreateResponse(StatusCode.MethodNotAllowed);
            }

            return context.CreateResponse(StatusCode.NotFound);
        }
        /// <summary>
        /// Creates a new named document, or creates a new revision of the existing document.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/common.html#put--db-docid
        /// <remarks>
        public static ICouchbaseResponseState UpdateDocument(ICouchbaseListenerContext context)
        {
            return PerformLogicWithDocumentBody(context, (db, body) =>
            {
                var response = context.CreateResponse();
                string docId = context.DocumentName;
                if(context.GetQueryParam<bool>("new_edits", bool.TryParse, true)) {
                    // Regular PUT:
                    return UpdateDb(context, db, docId, body, false);
                } else {
                    // PUT with new_edits=false -- forcible insertion of existing revision:
                    RevisionInternal rev = new RevisionInternal(body);
                    if(rev == null) {
                        response.InternalStatus = StatusCode.BadJson;
                        return response;
                    }

                    if(!docId.Equals(rev.GetDocId()) || rev.GetRevId() == null) {
                        response.InternalStatus = StatusCode.BadId;
                        return response;
                    }

                    var history = Database.ParseCouchDBRevisionHistory(body.GetProperties());
                    Status status = new Status();
                    try {
                      db.ForceInsert(rev, history, null);
                    } catch(CouchbaseLiteException e) {
                        status = e.CBLStatus;
                    }

                    if(!status.IsError) {
                        response.JsonBody = new Body(new Dictionary<string, object> {
                            { "ok", true },
                            { "id", rev.GetDocId() },
                            { "rev", rev.GetRevId() }
                        });
                    }

                    response.InternalStatus = status.Code;
                    return response;
                }
            }).AsDefaultState();
        }
        /// <summary>
        /// Returns the file attachment associated with the document. The raw data of the associated attachment is returned 
        /// (just as if you were accessing a static file. The returned Content-Type will be the same as the content type 
        /// set when the document attachment was submitted into the database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/attachments.html#get--db-docid-attname
        /// <remarks>
        public static ICouchbaseResponseState GetAttachment(ICouchbaseListenerContext context)
        {
            return DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                Status status = new Status();
                var rev = db.GetDocument(context.DocumentName, context.GetQueryParam("rev"), false, 
                    status);
                    
                if(rev ==null) {
                    return context.CreateResponse(status.Code);
                }
                if(context.CacheWithEtag(rev.GetRevId())) {
                    return context.CreateResponse(StatusCode.NotModified);
                }

                string acceptEncoding = context.RequestHeaders["Accept-Encoding"];
                bool acceptEncoded = acceptEncoding != null && acceptEncoding.Contains("gzip") &&
                    context.RequestHeaders["Range"] == null;

                var attachment = db.GetAttachmentForRevision(rev, context.AttachmentName, status);
                if(attachment == null) {
                    return context.CreateResponse(status.Code);
                }

                var response = context.CreateResponse();
                if(context.Method.Equals(HttpMethod.Head)) {
                    var length = attachment.Length;
                    if(acceptEncoded && attachment.Encoding == AttachmentEncoding.GZIP &&
                        attachment.EncodedLength > 0) {
                        length = attachment.EncodedLength;
                    }

                    response["Content-Length"] = length.ToString();
                } else {
                    var contents = acceptEncoded ? attachment.EncodedContent : attachment.Content;
                    if(contents == null) {
                        response.InternalStatus = StatusCode.NotFound;
                        return response;
                    }

                    response.BinaryBody = contents;
                }

                response["Content-Type"] = attachment.ContentType;
                if(acceptEncoded && attachment.Encoding == AttachmentEncoding.GZIP) {
                    response["Content-Encoding"] = "gzip";
                }

                return response;
            }).AsDefaultState();
        }
        // Update the given attachment using the provided info
        private static CouchbaseLiteResponse UpdateAttachment(ICouchbaseListenerContext context, Database db, 
            string attachment, string docId, BlobStoreWriter body)
        {
            RevisionInternal rev = db.UpdateAttachment(attachment, body, context.RequestHeaders["Content-Type"], AttachmentEncoding.None,
                    docId, context.GetQueryParam("rev") ?? context.IfMatch());

            var response = context.CreateResponse();
            response.JsonBody = new Body(new Dictionary<string, object> {
                { "ok", true },
                { "id", rev.GetDocId() },
                { "rev", rev.GetRevId() }
            });
            context.CacheWithEtag(rev.GetRevId());
            if (body != null) {
                response["Location"] = context.RequestUrl.AbsoluteUri;
            }

            return response;
        }
        /// <summary>
        /// Creates (and executes) a temporary view based on the view function supplied in the JSON request.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/temp-views.html#post--db-_temp_view
        /// </remarks>
        public static ICouchbaseResponseState ExecuteTemporaryViewFunction(ICouchbaseListenerContext context)
        {
            var response = context.CreateResponse();
            if (context.RequestHeaders["Content-Type"] == null || 
                !context.RequestHeaders["Content-Type"].StartsWith("application/json")) {
                response.InternalStatus = StatusCode.UnsupportedType;
                return response.AsDefaultState();
            }

            IEnumerable<byte> json = context.BodyStream.ReadAllBytes();
            var requestBody = new Body(json);
            if (!requestBody.IsValidJSON()) {
                response.InternalStatus = StatusCode.BadJson;
                return response.AsDefaultState();
            }

            var props = requestBody.GetProperties();
            if (props == null) {
                response.InternalStatus = StatusCode.BadJson;
                return response.AsDefaultState();
            }

            var options = context.QueryOptions;
            if (options == null) {
                response.InternalStatus = StatusCode.BadRequest;
                return response.AsDefaultState();
            }

            return PerformLogicWithDatabase(context, true, db =>
            {
                if (context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) {
                    response.InternalStatus = StatusCode.NotModified;
                    return response;
                }

                var view = db.GetView("@@TEMPVIEW@@");
                var status = view.Compile(props, "javascript");
                if(status.IsError) {
                    response.InternalStatus = status.Code;
                    return response;
                }

                try {
                    view.UpdateIndex_Internal();
                    return QueryView(context, null, view, options);
                } catch(CouchbaseLiteException e) {
                    response.InternalStatus = e.CBLStatus.Code;
                }

                return response;
            }).AsDefaultState();

        }
        /// <summary>
        /// The entry point for routing a request received by an CouchbaseLiteServiceListener
        /// </summary>
        /// <param name="context">The context containing information about the
        /// request</param>
        public void HandleRequest(ICouchbaseListenerContext context)
        {
            Log.V(TAG, "Processing {0} request to {1}", context.Method, context.RequestUrl.AbsoluteUri);
            var method = context.Method;

            if (OnAccessCheck != null) {
                Status result = null;
                try {
                    result = OnAccessCheck(method, context.RequestUrl.AbsolutePath);
                } catch(Exception e) {
                    result = new Status(StatusCode.Exception);
                    Log.E(TAG, "Unhandled non-Couchbase exception in OnAccessCheck", e);
                }

                if (result.IsError) {
                    var r = context.CreateResponse(result.Code);
                    ProcessResponse(context, r.AsDefaultState());
                    return;
                }
            }

            RestMethod logic = null;
            if (method.Equals("GET") || method.Equals("HEAD")) {
                logic = _Get.LogicForRequest(context.RequestUrl);
            } else if (method.Equals("POST")) {
                logic = _Post.LogicForRequest(context.RequestUrl);
            } else if (method.Equals("PUT")) {
                logic = _Put.LogicForRequest(context.RequestUrl);
            } else if (method.Equals("DELETE")) {
                logic = _Delete.LogicForRequest(context.RequestUrl);
            } else {
                logic = NOT_ALLOWED; // Shouldn't happen
            }

            ICouchbaseResponseState responseState = null;

            try {
                responseState = logic(context);
            } catch(Exception e) {
                var ce = e as CouchbaseLiteException;
                if (ce != null) {
                    // This is in place so that a response can be written simply by throwing a couchbase lite exception
                    // in the routing logic
                    Log.I(TAG, "Couchbase exception in routing logic, this message can be ignored if intentional", e);
                    responseState = context.CreateResponse(ce.CBLStatus.Code).AsDefaultState();
                } else {
                    Log.E(TAG, "Unhandled non-Couchbase exception in routing logic", e);
                    responseState = context.CreateResponse(StatusCode.Exception).AsDefaultState();
                }
            }

            ProcessResponse(context, responseState);
        }
        /// <summary>
        /// Performs the given logic with the specified database
        /// </summary>
        /// <returns>The result (in terms of response to the client) of the database operation</returns>
        /// <param name="context">The Couchbase Lite HTTP context</param>
        /// <param name="open">Whether or not to open the database, or just find it</param>
        /// <param name="action">The logic to perform on the database</param>
        public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open, 
            Func<Database, CouchbaseLiteResponse> action) 
        {
            string dbName = context.DatabaseName;
            Database db = context.DbManager.GetDatabase(dbName, false);
            if (db == null || !db.Exists()) {
                return context.CreateResponse(StatusCode.NotFound);
            }

            if (open) {
                try {
                    db.Open();
                } catch(CouchbaseLiteException e) {
                    Log.To.Listener.W(TAG, "Exception in PerformLogicWithDatabase, returning 500", e);
                    return context.CreateResponse(StatusCode.DbError);
                } catch(Exception e) {
                    Log.To.Listener.E(TAG, "Exception in PerformLogicWithDatabase, returning 500", e);
                    return context.CreateResponse(StatusCode.DbError);
                }
            }

            return action(db);
        }
        /// <summary>
        /// Creates a new database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/common.html#put--db
        /// </remarks>
        public static ICouchbaseResponseState UpdateConfiguration(ICouchbaseListenerContext context)
        {
            string dbName = context.DatabaseName;
            Database db = context.DbManager.GetDatabase(dbName, false);
            if (db != null && db.Exists()) {
                return context.CreateResponse(StatusCode.PreconditionFailed).AsDefaultState();
            }

            try {
                db.Open();
            } catch(CouchbaseLiteException) {
                return context.CreateResponse(StatusCode.Exception).AsDefaultState();
            }

            return context.CreateResponse(StatusCode.Created).AsDefaultState();
        }
        public static ICouchbaseResponseState RevsDiff(ICouchbaseListenerContext context)
        {
            // Collect all of the input doc/revision IDs as CBL_Revisions:
            var revs = new RevisionList();
            var body = context.BodyAs<Dictionary<string, object>>();
            if (body == null) {
                return context.CreateResponse(StatusCode.BadJson).AsDefaultState();
            }

            foreach (var docPair in body) {
                var revIDs = docPair.Value.AsList<string>();
                if (revIDs == null) {
                    return context.CreateResponse(StatusCode.BadParam).AsDefaultState();
                }

                foreach (var revID in revIDs) {
                    var rev = new RevisionInternal(docPair.Key, revID.AsRevID(), false);
                    revs.Add(rev);
                }
            }

            return PerformLogicWithDatabase(context, true, db =>
            {
                var response = context.CreateResponse();
                // Look them up, removing the existing ones from revs:
                db.Storage.FindMissingRevisions(revs);

                // Return the missing revs in a somewhat different format:
                IDictionary<string, object> diffs = new Dictionary<string, object>();
                foreach(var rev in revs) {
                    var docId = rev.DocID;
                    IList<RevisionID> missingRevs = null;
                    if(!diffs.ContainsKey(docId)) {
                        missingRevs = new List<RevisionID>();
                        diffs[docId] = new Dictionary<string, IList<RevisionID>> { { "missing", missingRevs } };
                    } else {
                        missingRevs = ((Dictionary<string, IList<RevisionID>>)diffs[docId])["missing"];
                    }

                    missingRevs.Add(rev.RevID);
                }

                // Add the possible ancestors for each missing revision:
                foreach(var docPair in diffs) {
                    IDictionary<string, IList<RevisionID>> docInfo = (IDictionary<string, IList<RevisionID>>)docPair.Value;
                    int maxGen = 0;
                    RevisionID maxRevID = null;
                    foreach(var revId in docInfo["missing"]) {
                        if(revId.Generation > maxGen) {
                            maxGen = revId.Generation;
                            maxRevID = revId;
                        }
                    }

                    var rev = new RevisionInternal(docPair.Key, maxRevID, false);
                    var ancestors = db.Storage.GetPossibleAncestors(rev, 0, ValueTypePtr<bool>.NULL)?.ToList();
                    if(ancestors != null && ancestors.Count > 0) {
                        docInfo["possible_ancestors"] = ancestors;
                    }
                }

                response.JsonBody = new Body(diffs);
                return response;
            }).AsDefaultState();

        }
        /// <summary>
        /// Returns a JSON structure of all of the documents in a given database.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/bulk-api.html#get--db-_all_docs
        /// </remarks>
        public static ICouchbaseResponseState GetAllDocuments(ICouchbaseListenerContext context)
        {
            return PerformLogicWithDatabase(context, true, db =>
            {
                if(context.CacheWithEtag(db.GetLastSequenceNumber().ToString())) {
                    return context.CreateResponse(StatusCode.NotModified);
                }

                var options = context.QueryOptions;
                if(options == null) {
                    return context.CreateResponse(StatusCode.BadParam);
                }

                return DoAllDocs(context, db, options);
            }).AsDefaultState();
        }
        /// <summary>
        /// Deletes the specified database, and all the documents and attachments contained within it.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/database/common.html#delete--db
        /// </remarks>
        public static ICouchbaseResponseState DeleteConfiguration(ICouchbaseListenerContext context) 
        {
            return PerformLogicWithDatabase(context, false, db =>
            {
                if(context.GetQueryParam("rev") != null) {
                    // CouchDB checks for this; probably meant to be a document deletion
                    return context.CreateResponse(StatusCode.BadId);
                }

                try {
                    db.Delete();
                } catch (CouchbaseLiteException) {
                    return context.CreateResponse(StatusCode.InternalServerError);
                }

                return context.CreateResponse();
            }).AsDefaultState();
        }
        // Factors out the logic of opening the database and reading the document body from the HTTP request
        // and performs the specified logic on the body received in the request, barring any problems
        private static CouchbaseLiteResponse PerformLogicWithDocumentBody(ICouchbaseListenerContext context, 
            Func<Database, Body, CouchbaseLiteResponse> callback)
        {
            return DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                MultipartDocumentReader reader = new MultipartDocumentReader(db);
                reader.SetContentType(context.RequestHeaders["Content-Type"]);
                reader.AppendData(context.BodyStream.ReadAllBytes());
                try {
                    reader.Finish();
                } catch(InvalidOperationException) {
                    return context.CreateResponse(StatusCode.BadRequest);
                }

                return callback(db, new Body(reader.GetDocumentProperties()));
            });
        }
        /// <summary>
        /// Uploads the supplied content as an attachment to the specified document.
        /// </summary>
        /// <returns>The response state for further HTTP processing</returns>
        /// <param name="context">The context of the Couchbase Lite HTTP request</param>
        /// <remarks>
        /// http://docs.couchdb.org/en/latest/api/document/attachments.html#put--db-docid-attname
        /// <remarks>
        public static ICouchbaseResponseState UpdateAttachment(ICouchbaseListenerContext context)
        {
            var state = new AsyncOpCouchbaseResponseState();
            DatabaseMethods.PerformLogicWithDatabase(context, true, db =>
            {
                
                var blob = db.AttachmentWriter;
                var httpBody = new byte[context.ContentLength];
                context.BodyStream.ReadAsync(httpBody, 0, httpBody.Length).ContinueWith(t => {
                    if(t.Result == 0) {
                        state.Response = context.CreateResponse(StatusCode.BadAttachment);
                        state.SignalFinished();
                        return;
                    }

                    blob.AppendData(httpBody);
                    blob.Finish();

                    state.Response = UpdateAttachment(context, db, context.AttachmentName, context.DocumentName, blob);
                    state.SignalFinished();
                });

                return null;
            });
                
            return state;
        }
        /// <summary>
        /// Performs the given logic with the specified database
        /// </summary>
        /// <returns>The result (in terms of response to the client) of the database operation</returns>
        /// <param name="context">The Couchbase Lite HTTP context</param>
        /// <param name="open">Whether or not to open the database, or just find it</param>
        /// <param name="action">The logic to perform on the database</param>
        public static CouchbaseLiteResponse PerformLogicWithDatabase(ICouchbaseListenerContext context, bool open, 
            Func<Database, CouchbaseLiteResponse> action) 
        {
            string dbName = context.DatabaseName;
            Database db = context.DbManager.GetDatabaseWithoutOpening(dbName, false);
            if (db == null || !db.Exists()) {
                return context.CreateResponse(StatusCode.NotFound);
            }

            if (open) {
                bool opened = db.Open();
                if (!opened) {
                    return context.CreateResponse(StatusCode.DbError);
                }
            }

            return action(db);
        }