You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2008/10/02 18:14:45 UTC

svn commit: r701174 - in /incubator/couchdb/trunk/src/couchdb: couch_httpd_db.erl couch_httpd_misc_handlers.erl couch_httpd_view.erl

Author: damien
Date: Thu Oct  2 09:14:45 2008
New Revision: 701174

URL: http://svn.apache.org/viewvc?rev=701174&view=rev
Log:
Added files forgotten in the httpd refactoring checkin.

Added:
    incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl
    incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl
    incubator/couchdb/trunk/src/couchdb/couch_httpd_view.erl

Added: incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl?rev=701174&view=auto
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl (added)
+++ incubator/couchdb/trunk/src/couchdb/couch_httpd_db.erl Thu Oct  2 09:14:45 2008
@@ -0,0 +1,577 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License.  You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_httpd_db).
+-include("couch_db.hrl").
+
+-export([handle_request/1, db_req/2]).
+
+-import(couch_httpd,
+    [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
+    start_json_response/2,send_chunk/2,end_json_response/1,
+    start_chunked_response/3]).
+
+-record(doc_query_args, {
+    options = [],
+    rev = "",
+    open_revs = ""
+}).
+    
+% Database request handlers
+handle_request(#httpd{path_parts=[DbName|RestParts],method=Method,
+        db_url_handlers=DbUrlHandlers}=Req)->
+    case {Method, RestParts} of
+    {'PUT', []} ->
+        create_db_req(Req, DbName);
+    {'DELETE', []} ->
+        delete_db_req(Req, DbName);
+    {_, []} ->
+        do_db_req(Req, fun db_req/2);
+    {_, [SecondPart|_]} ->
+        Handler = couch_util:dict_find(SecondPart, DbUrlHandlers, fun db_req/2),
+        do_db_req(Req, Handler)
+    end.
+
+create_db_req(Req, DbName) ->
+    case couch_server:create(DbName, []) of
+    {ok, Db} ->
+        couch_db:close(Db),
+        send_json(Req, 201, {[{ok, true}]});
+    Error ->
+        throw(Error)
+    end.
+
+delete_db_req(Req, DbName) ->
+    case couch_server:delete(DbName) of
+    ok ->
+        send_json(Req, 200, {[{ok, true}]});
+    Error ->
+        throw(Error)
+    end.
+
+do_db_req(#httpd{path_parts=[DbName|_]}=Req, Fun) ->
+    case couch_db:open(DbName, []) of
+    {ok, Db} ->
+        try
+            Fun(Req, Db)
+        after
+            couch_db:close(Db)
+        end;
+    Error ->
+        throw(Error)
+    end.
+
+db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) ->
+    {ok, DbInfo} = couch_db:get_db_info(Db),
+    send_json(Req, {DbInfo});
+
+db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) ->
+    Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
+    DocId = couch_util:new_uuid(),
+    {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=[]}, []),
+    send_json(Req, 201, {[
+        {ok, true},
+        {id, DocId},
+        {rev, NewRev}
+    ]});
+
+db_req(#httpd{path_parts=[_DbName]}=Req, _Db) ->
+    send_method_not_allowed(Req, "DELETE,GET,HEAD,POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
+    {JsonProps} = couch_httpd:json_body(Req),
+    DocsArray = proplists:get_value(<<"docs">>, JsonProps),
+    % convert all the doc elements to native docs
+    case proplists:get_value(<<"new_edits">>, JsonProps, true) of
+    true ->
+        Docs = lists:map(
+            fun({ObjProps} = JsonObj) ->
+                Doc = couch_doc:from_json_obj(JsonObj),
+                Id = case Doc#doc.id of
+                    <<>> -> couch_util:new_uuid();
+                    Id0 -> Id0
+                end,
+                Revs = case proplists:get_value(<<"_rev">>, ObjProps) of
+                    undefined -> [];
+                    Rev  -> [Rev]
+                end,
+                Doc#doc{id=Id,revs=Revs}
+            end,
+            DocsArray),
+        {ok, ResultRevs} = couch_db:update_docs(Db, Docs, []),
+
+        % output the results
+        DocResults = lists:zipwith(
+            fun(Doc, NewRev) ->
+                {[{"id", Doc#doc.id}, {"rev", NewRev}]}
+            end,
+            Docs, ResultRevs),
+        send_json(Req, 201, {[
+            {ok, true},
+            {new_revs, DocResults}
+        ]});
+
+    false ->
+        Options =
+        case proplists:get_value(<<"new_edits">>, JsonProps, true) of
+            true -> [new_edits];
+            _ -> []
+        end,
+        Docs = [couch_doc:from_json_obj(JsonObj) || JsonObj <- DocsArray],
+        ok = couch_db:save_docs(Db, Docs, Options),
+        send_json(Req, 201, {[
+            {ok, true}
+        ]})
+    end;
+db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_compact">>]}=Req, Db) ->
+    ok = couch_db:start_compact(Db),
+    send_json(Req, 202, {[{ok, true}]});
+
+db_req(#httpd{path_parts=[_,<<"_compact">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) ->
+    {IdsRevs} = couch_httpd:json_body(Req),
+    % validate the json input
+    [{_Id, [_|_]=_Revs} = IdRevs || IdRevs <- IdsRevs],
+    
+    case couch_db:purge_docs(Db, IdsRevs) of
+    {ok, PurgeSeq, PurgedIdsRevs} ->
+        send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs}}]});
+    Error ->
+        throw(Error)
+    end;
+
+db_req(#httpd{path_parts=[_,<<"_purge">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs">>]}=Req, Db) ->
+    #view_query_args{
+        start_key = StartKey,
+        start_docid = StartDocId,
+        count = Count,
+        skip = SkipCount,
+        direction = Dir
+    } = QueryArgs = couch_httpd_view:parse_view_query(Req),
+    {ok, Info} = couch_db:get_db_info(Db),
+    TotalRowCount = proplists:get_value(doc_count, Info),
+
+    StartId = if is_binary(StartKey) -> StartKey;
+    true -> StartDocId
+    end,
+
+    FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, TotalRowCount,
+        fun couch_db:enum_docs_reduce_to_count/1),
+    AdapterFun = fun(#full_doc_info{id=Id}=FullDocInfo, Offset, Acc) ->
+        case couch_doc:to_doc_info(FullDocInfo) of
+        #doc_info{deleted=false, rev=Rev} ->
+            FoldlFun({{Id, Id}, {[{rev, Rev}]}}, Offset, Acc);
+        #doc_info{deleted=true} ->
+            {ok, Acc}
+        end
+    end,
+    {ok, FoldResult} = couch_db:enum_docs(Db, StartId, Dir, AdapterFun,
+            {Count, SkipCount, undefined, []}),
+    couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult});
+
+db_req(#httpd{path_parts=[_,<<"_all_docs">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "GET,HEAD");
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) ->
+    #view_query_args{
+        start_key = StartKey,
+        count = Count,
+        skip = SkipCount,
+        direction = Dir
+    } = QueryArgs = couch_httpd_view:parse_view_query(Req),
+
+    {ok, Info} = couch_db:get_db_info(Db),
+    TotalRowCount = proplists:get_value(doc_count, Info),
+
+    FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, TotalRowCount,
+            fun couch_db:enum_docs_since_reduce_to_count/1),
+    StartKey2 = case StartKey of
+        nil -> 0;
+        <<>> -> 100000000000;
+        StartKey when is_integer(StartKey) -> StartKey
+    end,
+    {ok, FoldResult} = couch_db:enum_docs_since(Db, StartKey2, Dir,
+        fun(DocInfo, Offset, Acc) ->
+            #doc_info{
+                id=Id,
+                rev=Rev,
+                update_seq=UpdateSeq,
+                deleted=Deleted,
+                conflict_revs=ConflictRevs,
+                deleted_conflict_revs=DelConflictRevs
+            } = DocInfo,
+            Json = {
+                [{"rev", Rev}] ++
+                case ConflictRevs of
+                    []  ->  [];
+                    _   ->  [{"conflicts", ConflictRevs}]
+                end ++
+                case DelConflictRevs of
+                    []  ->  [];
+                    _   ->  [{"deleted_conflicts", DelConflictRevs}]
+                end ++
+                case Deleted of
+                    true -> [{"deleted", true}];
+                    false -> []
+                end
+            },
+            FoldlFun({{UpdateSeq, Id}, Json}, Offset, Acc)
+        end, {Count, SkipCount, undefined, []}),
+    couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult});
+
+db_req(#httpd{path_parts=[_,<<"_all_docs_by_seq">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "GET,HEAD");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) ->
+    {JsonDocIdRevs} = couch_httpd:json_body(Req),
+    {ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs),
+    send_json(Req, {[
+        {missing_revs, {Results}}
+    ]});
+
+db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[DbName,<<"_design">>,Name|Rest]}=Req,
+        Db) ->
+    % Special case to enable using an unencoded in the URL of design docs, as
+    % slashes in document IDs must otherwise be URL encoded
+    db_req(Req#httpd{path_parts=[DbName,<<"_design/",Name/binary>>|Rest]}, Db);
+
+db_req(#httpd{path_parts=[_, DocId]}=Req, Db) ->
+    db_doc_req(Req, Db, DocId);
+
+db_req(#httpd{path_parts=[_, DocId, FileName]}=Req, Db) ->
+    db_attachment_req(Req, Db, DocId, FileName).
+
+
+
+db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) ->
+    case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+    missing_rev ->
+        couch_httpd:send_error(Req, 412, <<"missing_rev">>,
+            <<"Document rev/etag must be specified to delete">>);
+    RevToDelete ->
+        {ok, NewRev} = couch_db:delete_doc(Db, DocId, [RevToDelete]),
+        send_json(Req, 200, {[
+            {ok, true},
+            {id, DocId},
+            {rev, NewRev}
+            ]})
+    end;
+
+db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
+    #doc_query_args{
+        rev = Rev,
+        open_revs = Revs,
+        options = Options
+    } = parse_doc_query(Req),
+    case Revs of
+    [] ->
+        Doc = couch_doc_open(Db, DocId, Rev, Options),
+        DiskEtag = couch_httpd:doc_etag(Doc),
+        EtagsToMatch = string:tokens(
+                    couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
+        case lists:member(DiskEtag, EtagsToMatch) of
+        true ->
+            % the client has this in their cache.
+            couch_httpd:send_response(Req, 304, [{"Etag", DiskEtag}], <<>>);
+        false ->
+            Headers =
+            case Doc#doc.meta of
+            [] -> [{"Etag", DiskEtag}]; % output etag only when we have no meta
+            _ -> []
+            end,
+            send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
+        end;
+    _ ->
+        {ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options),
+        {ok, Resp} = start_json_response(Req, 200),
+        send_chunk(Resp, "["),
+        % We loop through the docs. The first time through the separator
+        % is whitespace, then a comma on subsequent iterations.
+        lists:foldl(
+            fun(Result, AccSeparator) ->
+                case Result of
+                {ok, Doc} ->
+                    JsonDoc = couch_doc:to_json_obj(Doc, Options),
+                    Json = ?JSON_ENCODE({[{ok, JsonDoc}]}),
+                    send_chunk(Resp, AccSeparator ++ Json);
+                {{not_found, missing}, RevId} ->
+                    Json = ?JSON_ENCODE({[{"missing", RevId}]}),
+                    send_chunk(Resp, AccSeparator ++ Json)
+                end,
+                "," % AccSeparator now has a comma
+            end,
+            "", Results),
+        send_chunk(Resp, "]"),
+        end_json_response(Resp)
+    end;
+
+db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
+    Form = couch_httpd:parse_form(Req),
+    Rev = list_to_binary(proplists:get_value("_rev", Form)),
+    Doc = case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
+        {ok, [{ok, Doc0}]}  -> Doc0#doc{revs=[Rev]};
+        {ok, [Error]}       -> throw(Error)
+    end,
+
+    NewAttachments = [
+        {list_to_binary(Name), {list_to_binary(ContentType), Content}} ||
+        {Name, {ContentType, _}, Content} <-
+        proplists:get_all_values("_attachments", Form)
+    ],
+    #doc{attachments=Attachments} = Doc,
+    NewDoc = Doc#doc{
+        attachments = Attachments ++ NewAttachments
+    },
+    {ok, NewRev} = couch_db:update_doc(Db, NewDoc, []),
+
+    send_json(Req, 201, [{"Etag", "\"" ++ NewRev ++ "\""}], {obj, [
+        {ok, true},
+        {id, DocId},
+        {rev, NewRev}
+    ]});
+
+db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
+    Json = couch_httpd:json_body(Req),
+    Doc = couch_doc:from_json_obj(Json),
+    ExplicitRev =
+    case Doc#doc.revs of
+        [Rev0|_] -> Rev0;
+        [] -> undefined
+    end,
+    case extract_header_rev(Req, ExplicitRev) of
+    missing_rev ->
+        Revs = [];
+    Rev ->
+        Revs = [Rev]
+    end,
+    {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=Revs}, []),
+    send_json(Req, 201, [{"Etag", <<"\"", NewRev/binary, "\"">>}], {[
+        {ok, true},
+        {id, DocId},
+        {rev, NewRev}
+    ]});
+
+db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) ->
+    SourceRev =
+    case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+        missing_rev -> [];
+        Rev -> Rev
+    end,
+
+    {TargetDocId, TargetRev} = parse_copy_destination_header(Req),
+
+    % open revision Rev or Current  
+    Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
+
+    % save new doc
+    {ok, NewTargetRev} = couch_db:update_doc(Db, Doc#doc{id=TargetDocId, revs=TargetRev}, []),
+
+    send_json(Req, 201, [{"Etag", "\"" ++ binary_to_list(NewTargetRev) ++ "\""}], {[
+        {ok, true},
+        {id, TargetDocId},
+        {rev, NewTargetRev}
+    ]});
+
+db_doc_req(#httpd{method='MOVE'}=Req, Db, SourceDocId) ->
+    SourceRev =
+    case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+    missing_rev -> 
+        throw({bad_request, "MOVE requires a specified rev parameter"
+                "for the origin resource."});
+    Rev -> Rev
+    end,
+
+    {TargetDocId, TargetRev} = parse_copy_destination_header(Req),
+    % open revision Rev or Current
+    Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
+
+    % save new doc & delete old doc in one operation
+    Docs = [
+        Doc#doc{id=TargetDocId, revs=TargetRev},
+        #doc{id=SourceDocId, revs=[SourceRev], deleted=true}
+        ],
+
+    {ok, ResultRevs} = couch_db:update_docs(Db, Docs, []),
+
+    DocResults = lists:zipwith(
+        fun(FDoc, NewRev) ->
+            {[{id, FDoc#doc.id}, {rev, NewRev}]}
+        end,
+        Docs, ResultRevs),
+    send_json(Req, 201, {[
+        {ok, true},
+        {new_revs, DocResults}
+    ]});
+
+db_doc_req(Req, _Db, _DocId) ->
+    send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY,MOVE").
+
+% Useful for debugging
+% couch_doc_open(Db, DocId) ->
+%   couch_doc_open(Db, DocId, [], []).
+
+couch_doc_open(Db, DocId, Rev, Options) ->
+    case Rev of
+    "" -> % open most recent rev
+        case couch_db:open_doc(Db, DocId, Options) of
+        {ok, Doc} ->
+            Doc;
+         Error ->
+             throw(Error)
+         end;
+  _ -> % open a specific rev (deletions come back as stubs)
+      case couch_db:open_doc_revs(Db, DocId, [Rev], Options) of
+          {ok, [{ok, Doc}]} ->
+              Doc;
+          {ok, [Else]} ->
+              throw(Else)
+      end
+  end.
+
+% Attachment request handlers
+
+db_attachment_req(#httpd{method='GET'}=Req, Db, DocId, FileName) ->
+    case couch_db:open_doc(Db, DocId, []) of
+    {ok, #doc{attachments=Attachments}} ->
+        case proplists:get_value(FileName, Attachments) of
+        undefined ->
+            throw({not_found, "Document is missing attachment"});
+        {Type, Bin} ->
+            {ok, Resp} = start_chunked_response(Req, 200, [
+                {"Cache-Control", "must-revalidate"},
+                {"Content-Type", binary_to_list(Type)},
+                {"Content-Length", integer_to_list(couch_doc:bin_size(Bin))}]),
+            couch_doc:bin_foldl(Bin,
+                fun(BinSegment, []) ->
+                    send_chunk(Resp, BinSegment),
+                    {ok, []}
+                end,
+                []
+            ),
+            send_chunk(Resp, "")
+        end;
+    Error ->
+        throw(Error)
+    end;
+
+db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileName)
+        when (Method == 'PUT') or (Method == 'DELETE') ->
+
+    NewAttachment = case Method of
+        'DELETE' ->
+            [];
+        _ ->
+            [{FileName, {
+                list_to_binary(couch_httpd:header_value(Req,"Content-Type")),
+                couch_httpd:body(Req)
+            }}]
+    end,
+
+    Doc = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+        missing_rev -> % make the new doc
+            #doc{id=DocId};
+        Rev ->
+            case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
+            {ok, [{ok, Doc0}]}  -> Doc0#doc{revs=[Rev]};
+            {ok, [Error]}       -> throw(Error)
+            end
+    end,
+
+    #doc{attachments=Attachments} = Doc,
+    DocEdited = Doc#doc{
+        attachments = NewAttachment ++ proplists:delete(FileName, Attachments)
+    },
+    {ok, UpdatedRev} = couch_db:update_doc(Db, DocEdited, []),
+    send_json(Req, case Method of 'DELETE' -> 200; _ -> 201 end, {[
+        {ok, true},
+        {id, DocId},
+        {rev, UpdatedRev}
+    ]});
+
+db_attachment_req(Req, _Db, _DocId, _FileName) ->
+    send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT").
+
+
+parse_doc_query(Req) ->
+    lists:foldl(fun({Key,Value}, Args) ->
+        case {Key, Value} of
+        {"attachments", "true"} ->
+            Options = [attachments | Args#doc_query_args.options],
+            Args#doc_query_args{options=Options};
+        {"meta", "true"} ->
+            Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options],
+            Args#doc_query_args{options=Options};
+        {"revs", "true"} ->
+            Options = [revs | Args#doc_query_args.options],
+            Args#doc_query_args{options=Options};
+        {"revs_info", "true"} ->
+            Options = [revs_info | Args#doc_query_args.options],
+            Args#doc_query_args{options=Options};
+        {"conflicts", "true"} ->
+            Options = [conflicts | Args#doc_query_args.options],
+            Args#doc_query_args{options=Options};
+        {"deleted_conflicts", "true"} ->
+            Options = [deleted_conflicts | Args#doc_query_args.options],
+            Args#doc_query_args{options=Options};
+        {"rev", Rev} ->
+            Args#doc_query_args{rev=list_to_binary(Rev)};
+        {"open_revs", "all"} ->
+            Args#doc_query_args{open_revs=all};
+        {"open_revs", RevsJsonStr} ->
+            JsonArray = ?JSON_DECODE(RevsJsonStr),
+            Args#doc_query_args{open_revs=JsonArray};
+        _Else -> % unknown key value pair, ignore.
+            Args
+        end
+    end, #doc_query_args{}, couch_httpd:qs(Req)).
+
+
+
+extract_header_rev(Req, ExplictRev) when is_list(ExplictRev)->
+    extract_header_rev(Req, list_to_binary(ExplictRev));
+extract_header_rev(Req, ExplictRev) ->
+    Etag = case couch_httpd:header_value(Req, "If-Match") of
+        undefined -> undefined;
+        Tag -> string:strip(Tag, both, $")
+    end,
+    case {ExplictRev, Etag} of
+    {undefined, undefined} -> missing_rev;
+    {_, undefined} -> ExplictRev;
+    {undefined, _} -> list_to_binary(Etag);
+    _ when ExplictRev == Etag -> list_to_binary(Etag);
+    _ ->
+        throw({bad_request, "Document rev and etag have different values"})
+    end.
+
+
+parse_copy_destination_header(Req) ->
+    Destination = couch_httpd:header_value(Req, "Destination"),
+    case regexp:match(Destination, "\\?") of
+    nomatch -> 
+        {list_to_binary(Destination), []};
+    {match, _, _} ->
+        {ok, [DocId, RevQueryOptions]} = regexp:split(Destination, "\\?"),
+        {ok, [_RevQueryKey, Rev]} = regexp:split(RevQueryOptions, "="),
+        {list_to_binary(DocId), [list_to_binary(Rev)]}
+    end.
+

Added: incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl?rev=701174&view=auto
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl (added)
+++ incubator/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl Thu Oct  2 09:14:45 2008
@@ -0,0 +1,142 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License.  You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_httpd_misc_handlers).
+
+-export([handle_welcome_req/2,handle_utils_dir_req/2,handle_all_dbs_req/1,
+    handle_replicate_req/1,handle_restart_req/1,handle_uuids_req/1,
+    handle_config_req/1]).
+    
+-export([increment_update_seq_req/2]).
+
+
+-include("couch_db.hrl").
+
+-import(couch_httpd,
+    [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
+    start_json_response/2,send_chunk/2,end_json_response/1,
+    start_chunked_response/3]).
+
+% httpd global handlers
+
+handle_welcome_req(#httpd{method='GET'}=Req, WelcomeMessage) ->
+    send_json(Req, {[
+        {couchdb, WelcomeMessage},
+        {version, list_to_binary(couch_server:get_version())}
+    ]});
+handle_welcome_req(Req, _) ->
+    send_method_not_allowed(Req, "GET,HEAD").
+
+
+handle_utils_dir_req(#httpd{method='GET'}=Req, DocumentRoot) ->
+    "/" ++ UrlPath = couch_httpd:path(Req),
+    case couch_httpd:partition(UrlPath) of
+    {_ActionKey, "/", RelativePath} ->
+        % GET /_utils/path or GET /_utils/
+        couch_httpd:serve_file(Req, RelativePath, DocumentRoot);
+    {_ActionKey, "", _RelativePath} ->
+        % GET /_utils
+        couch_httpd:send_response(Req, 301, [{"Location", "/_utils/"}], <<>>)
+    end;
+handle_utils_dir_req(Req, _) ->
+    send_method_not_allowed(Req, "GET,HEAD").
+
+
+handle_all_dbs_req(#httpd{method='GET'}=Req) ->
+    {ok, DbNames} = couch_server:all_databases(),
+    send_json(Req, DbNames);
+handle_all_dbs_req(Req) ->
+    send_method_not_allowed(Req, "GET,HEAD").
+
+
+handle_replicate_req(#httpd{method='POST'}=Req) ->
+    {Props} = couch_httpd:json_body(Req),
+    Source = proplists:get_value(<<"source">>, Props),
+    Target = proplists:get_value(<<"target">>, Props),
+    {Options} = proplists:get_value(<<"options">>, Props, {[]}),
+    {ok, {JsonResults}} = couch_rep:replicate(Source, Target, Options),
+    send_json(Req, {[{ok, true} | JsonResults]});
+handle_replicate_req(Req) ->
+    send_method_not_allowed(Req, "POST").
+
+
+handle_restart_req(#httpd{method='POST'}=Req) ->
+    Response = send_json(Req, {[{ok, true}]}),
+    spawn(fun() -> couch_server:remote_restart() end),
+    Response;
+handle_restart_req(Req) ->
+    send_method_not_allowed(Req, "POST").
+
+
+handle_uuids_req(#httpd{method='POST'}=Req) ->
+    Count = list_to_integer(couch_httpd:qs_value(Req, "count", "1")),
+    % generate the uuids
+    UUIDs = [ couch_util:new_uuid() || _ <- lists:seq(1,Count)],
+    % send a JSON response
+    send_json(Req, {[{"uuids", UUIDs}]});
+handle_uuids_req(Req) ->
+    send_method_not_allowed(Req, "POST").
+
+
+% Config request handler
+
+    
+% GET /_config/
+% GET /_config
+handle_config_req(#httpd{method='GET', path_parts=[_]}=Req) ->
+    KVs = [{list_to_binary(Key), list_to_binary(Value)}
+            || {Key, Value} <- couch_config:all()],
+    send_json(Req, 200, {KVs});
+% GET /_config/Section
+handle_config_req(#httpd{method='GET', path_parts=[_,Section]}=Req) ->
+    KVs = [{list_to_binary(Key), list_to_binary(Value)}
+            || {Key, Value} <- couch_config:get(Section)],
+    send_json(Req, 200, {KVs});
+% PUT /_config/Section/Key
+% "value"
+handle_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req) ->
+    Value = binary_to_list(couch_httpd:body(Req)),
+    ok = couch_config:set(Section, Key, Value),
+    send_json(Req, 200, {[
+        {ok, true}
+    ]});
+% GET /_config/Section/Key
+handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) ->
+    case couch_config:get(Section, Key, null) of
+    null ->
+        throw({not_found, unknown_config_value});
+    Value ->
+        send_json(Req, 200, list_to_binary(Value))
+    end;
+% DELETE /_config/Section/Key
+handle_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req) ->
+    case couch_config:get(Section, Key, null) of
+    null ->
+        throw({not_found, unknown_config_value});
+    OldValue ->
+        couch_config:delete(Section, Key),
+        send_json(Req, 200, list_to_binary(OldValue))
+    end;
+handle_config_req(Req) ->
+    send_method_not_allowed(Req, "GET,PUT,DELETE").
+
+
+% httpd db handlers
+
+increment_update_seq_req(#httpd{method='POST'}=Req, Db) ->
+    {ok, NewSeq} = couch_db:increment_update_seq(Db),
+    send_json(Req, {[{ok, true},
+        {update_seq, NewSeq}
+    ]});
+increment_update_seq_req(Req, _Db) ->
+    send_method_not_allowed(Req, "GET,PUT,DELETE").
+

Added: incubator/couchdb/trunk/src/couchdb/couch_httpd_view.erl
URL: http://svn.apache.org/viewvc/incubator/couchdb/trunk/src/couchdb/couch_httpd_view.erl?rev=701174&view=auto
==============================================================================
--- incubator/couchdb/trunk/src/couchdb/couch_httpd_view.erl (added)
+++ incubator/couchdb/trunk/src/couchdb/couch_httpd_view.erl Thu Oct  2 09:14:45 2008
@@ -0,0 +1,340 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License.  You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_httpd_view).
+-include("couch_db.hrl").
+
+-export([handle_view_req/2,handle_temp_view_req/2]).
+
+-export([parse_view_query/1,make_view_fold_fun/4,finish_view_fold/3]).
+
+-import(couch_httpd,
+    [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
+    start_json_response/2,send_chunk/2,end_json_response/1]).
+
+
+handle_view_req(#httpd{method='GET',path_parts=[_,_, Id, ViewName]}=Req, Db) ->
+    #view_query_args{
+        start_key = StartKey,
+        count = Count,
+        skip = SkipCount,
+        direction = Dir,
+        start_docid = StartDocId,
+        reduce = Reduce
+    } = QueryArgs = parse_view_query(Req),
+    
+    case couch_view:get_map_view({couch_db:name(Db), 
+            <<"_design/", Id/binary>>, ViewName}) of
+    {ok, View} ->    
+        {ok, RowCount} = couch_view:get_row_count(View),
+        Start = {StartKey, StartDocId},
+        FoldlFun = make_view_fold_fun(Req, QueryArgs, RowCount,
+                fun couch_view:reduce_to_count/1),
+        FoldAccInit = {Count, SkipCount, undefined, []},
+        FoldResult = couch_view:fold(View, Start, Dir, FoldlFun, FoldAccInit),
+        finish_view_fold(Req, RowCount, FoldResult);
+    {not_found, Reason} ->
+        case couch_view:get_reduce_view({couch_db:name(Db),
+                <<"_design/", Id/binary>>, ViewName}) of
+        {ok, View} ->
+            case Reduce of
+            false ->
+                {reduce, _N, _Lang, MapView} = View,
+                {ok, RowCount} = couch_view:get_row_count(MapView),
+                Start = {StartKey, StartDocId},
+                FoldlFun = make_view_fold_fun(Req, QueryArgs, RowCount,
+                    fun couch_view:reduce_to_count/1),
+                FoldAccInit = {Count, SkipCount, undefined, []},
+                FoldResult = couch_view:fold(MapView, Start, Dir, FoldlFun, FoldAccInit),
+                finish_view_fold(Req, RowCount, FoldResult);
+            _ ->
+                output_reduce_view(Req, View)
+            end;
+        _ ->
+            throw({not_found, Reason})
+        end
+    end;
+
+handle_view_req(Req, _Db) ->
+    send_method_not_allowed(Req, "GET,HEAD").
+
+handle_temp_view_req(#httpd{method='POST'}=Req, Db) ->
+    #view_query_args{
+        start_key = StartKey,
+        count = Count,
+        skip = SkipCount,
+        direction = Dir,
+        start_docid = StartDocId
+    } = QueryArgs = parse_view_query(Req),
+
+    case couch_httpd:primary_header_value(Req, "content-type") of
+        undefined -> ok;
+        "application/json" -> ok;
+        Else -> throw({incorrect_mime_type, Else})
+    end,
+    {Props} = couch_httpd:json_body(Req),
+    Language = proplists:get_value(<<"language">>, Props, <<"javascript">>),
+    MapSrc = proplists:get_value(<<"map">>, Props),
+    case proplists:get_value(<<"reduce">>, Props, null) of
+    null ->
+        {ok, View} = couch_view:get_map_view({temp, couch_db:name(Db), Language, MapSrc}),
+        Start = {StartKey, StartDocId},
+        
+        {ok, TotalRows} = couch_view:get_row_count(View),
+        
+        FoldlFun = make_view_fold_fun(Req, QueryArgs, TotalRows,
+                fun couch_view:reduce_to_count/1),
+        FoldAccInit = {Count, SkipCount, undefined, []},
+        FoldResult = couch_view:fold(View, Start, Dir, fun(A, B, C) ->
+            FoldlFun(A, B, C)
+        end, FoldAccInit),
+        finish_view_fold(Req, TotalRows, FoldResult);
+
+    RedSrc ->
+        {ok, View} = couch_view:get_reduce_view(
+                {temp,  couch_db:name(Db), Language, MapSrc, RedSrc}),
+        output_reduce_view(Req, View)
+    end;
+
+handle_temp_view_req(Req, _Db) ->
+    send_method_not_allowed(Req, "POST").
+
+
+output_reduce_view(Req, View) ->
+    #view_query_args{
+        start_key = StartKey,
+        end_key = EndKey,
+        count = Count,
+        skip = Skip,
+        direction = Dir,
+        start_docid = StartDocId,
+        end_docid = EndDocId,
+        group_level = GroupLevel
+    } = parse_view_query(Req),
+    GroupRowsFun =
+        fun({_Key1,_}, {_Key2,_}) when GroupLevel == 0 ->
+            true;
+        ({Key1,_}, {Key2,_})
+                when is_integer(GroupLevel) and is_list(Key1) and is_list(Key2) ->
+            lists:sublist(Key1, GroupLevel) == lists:sublist(Key2, GroupLevel);
+        ({Key1,_}, {Key2,_}) ->
+            Key1 == Key2
+        end,
+    {ok, Resp} = start_json_response(Req, 200),
+    send_chunk(Resp, "{\"rows\":["),
+    {ok, _} = couch_view:fold_reduce(View, Dir, {StartKey, StartDocId}, {EndKey, EndDocId},
+        GroupRowsFun,
+        fun(_Key, _Red, {AccSeparator,AccSkip,AccCount}) when AccSkip > 0 ->
+            {ok, {AccSeparator,AccSkip-1,AccCount}};
+        (_Key, _Red, {AccSeparator,0,AccCount}) when AccCount == 0 ->
+            {stop, {AccSeparator,0,AccCount}};
+        (_Key, Red, {AccSeparator,0,AccCount}) when GroupLevel == 0 ->
+            Json = ?JSON_ENCODE({[{key, null}, {value, Red}]}),
+            send_chunk(Resp, AccSeparator ++ Json),
+            {ok, {",",0,AccCount-1}};
+        (Key, Red, {AccSeparator,0,AccCount})
+                when is_integer(GroupLevel) 
+                andalso is_list(Key) ->
+            Json = ?JSON_ENCODE(
+                {[{key, lists:sublist(Key, GroupLevel)},{value, Red}]}),
+            send_chunk(Resp, AccSeparator ++ Json),
+            {ok, {",",0,AccCount-1}};
+        (Key, Red, {AccSeparator,0,AccCount}) ->
+            Json = ?JSON_ENCODE({[{key, Key}, {value, Red}]}),
+            send_chunk(Resp, AccSeparator ++ Json),
+            {ok, {",",0,AccCount-1}}
+        end, {"", Skip, Count}),
+    send_chunk(Resp, "]}"),
+    end_json_response(Resp).
+    
+
+reverse_key_default(nil) -> {};
+reverse_key_default({}) -> nil;
+reverse_key_default(Key) -> Key.
+
+parse_view_query(Req) ->
+    QueryList = couch_httpd:qs(Req),
+    lists:foldl(fun({Key,Value}, Args) ->
+        case {Key, Value} of
+        {"", _} ->
+            Args;
+        {"key", Value} ->
+            JsonKey = ?JSON_DECODE(Value),
+            Args#view_query_args{start_key=JsonKey,end_key=JsonKey};
+        {"startkey_docid", DocId} ->
+            Args#view_query_args{start_docid=list_to_binary(DocId)};
+        {"endkey_docid", DocId} ->
+            Args#view_query_args{end_docid=list_to_binary(DocId)};
+        {"startkey", Value} ->
+            Args#view_query_args{start_key=?JSON_DECODE(Value)};
+        {"endkey", Value} ->
+            Args#view_query_args{end_key=?JSON_DECODE(Value)};
+        {"count", Value} ->
+            case (catch list_to_integer(Value)) of
+            Count when is_integer(Count) ->
+                if Count < 0 ->
+                    Args#view_query_args {
+                        direction =
+                        if Args#view_query_args.direction == rev -> fwd;
+                        true -> rev
+                        end,
+                        count=Count,
+                        start_key = reverse_key_default(Args#view_query_args.start_key),
+                        start_docid = reverse_key_default(Args#view_query_args.start_docid),
+                        end_key = reverse_key_default(Args#view_query_args.end_key),
+                        end_docid =  reverse_key_default(Args#view_query_args.end_docid)};
+                true ->
+                    Args#view_query_args{count=Count}
+                end;
+            _Error ->
+                Msg = io_lib:format("Bad URL query value, number expected: count=~s", [Value]),
+                throw({query_parse_error, Msg})
+            end;
+        {"update", "false"} ->
+            Args#view_query_args{update=false};
+        {"descending", "true"} ->
+            case Args#view_query_args.direction of
+            fwd ->
+                Args#view_query_args {
+                    direction = rev,
+                    start_key = reverse_key_default(Args#view_query_args.start_key),
+                    start_docid = reverse_key_default(Args#view_query_args.start_docid),
+                    end_key = reverse_key_default(Args#view_query_args.end_key),
+                    end_docid =  reverse_key_default(Args#view_query_args.end_docid)};
+            _ ->
+                Args %already reversed
+            end;
+        {"descending", "false"} ->
+          % The descending=false behaviour is the default behaviour, so we
+          % simpply ignore it. This is only for convenience when playing with
+          % the HTTP API, so that a user doesn't get served an error when
+          % flipping true to false in the descending option.
+          Args;
+        {"skip", Value} ->
+            case (catch list_to_integer(Value)) of
+            Count when is_integer(Count) ->
+                Args#view_query_args{skip=Count};
+            _Error ->
+                Msg = lists:flatten(io_lib:format(
+                "Bad URL query value, number expected: skip=~s", [Value])),
+                throw({query_parse_error, Msg})
+            end;
+        {"group", "true"} ->
+            Args#view_query_args{group_level=exact};
+        {"group_level", LevelStr} ->
+            Args#view_query_args{group_level=list_to_integer(LevelStr)};
+        {"reduce", "true"} ->
+            Args#view_query_args{reduce=true};
+        {"reduce", "false"} ->
+            Args#view_query_args{reduce=false};
+        _ -> % unknown key
+            Msg = lists:flatten(io_lib:format(
+                "Bad URL query key:~s", [Key])),
+            throw({query_parse_error, Msg})
+        end
+    end, #view_query_args{}, QueryList).
+
+
+make_view_fold_fun(Req, QueryArgs, TotalViewCount, ReduceCountFun) ->
+    #view_query_args{
+        end_key = EndKey,
+        end_docid = EndDocId,
+        direction = Dir,
+        count = Count
+    } = QueryArgs,
+
+    PassedEndFun =
+    case Dir of
+    fwd ->
+        fun(ViewKey, ViewId) ->
+            couch_view:less_json([EndKey, EndDocId], [ViewKey, ViewId])
+        end;
+    rev->
+        fun(ViewKey, ViewId) ->
+            couch_view:less_json([ViewKey, ViewId], [EndKey, EndDocId])
+        end
+    end,
+
+    NegCountFun = fun({{Key, DocId}, Value}, OffsetReds,
+                      {AccCount, AccSkip, Resp, AccRevRows}) ->
+        Offset = ReduceCountFun(OffsetReds),
+        PassedEnd = PassedEndFun(Key, DocId),
+        case {PassedEnd, AccCount, AccSkip, Resp} of
+        {true, _, _, _} -> % The stop key has been passed, stop looping.
+            {stop, {AccCount, AccSkip, Resp, AccRevRows}};
+        {_, 0, _, _} -> % we've done "count" rows, stop foldling
+            {stop, {0, 0, Resp, AccRevRows}};
+        {_, _, AccSkip, _} when AccSkip > 0 ->
+            {ok, {AccCount, AccSkip - 1, Resp, AccRevRows}};
+        {_, _, _, undefined} ->
+            {ok, Resp2} = start_json_response(Req, 200),
+            Offset2 = TotalViewCount - Offset -
+                lists:min([TotalViewCount - Offset, - AccCount]),
+            JsonBegin = io_lib:format("{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n",
+                    [TotalViewCount, Offset2]),
+            send_chunk(Resp2, JsonBegin),
+            JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+            {ok, {AccCount + 1, 0, Resp2, [?JSON_ENCODE(JsonObj) | AccRevRows]}};
+        {_, AccCount, _, Resp} ->
+            JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+            {ok, {AccCount + 1, 0, Resp, [?JSON_ENCODE(JsonObj), ",\r\n" | AccRevRows]}}
+        end
+    end,
+
+    PosCountFun = fun({{Key, DocId}, Value}, OffsetReds,
+                      {AccCount, AccSkip, Resp, AccRevRows}) ->
+        Offset = ReduceCountFun(OffsetReds), % I think we only need this call once per view
+        PassedEnd = PassedEndFun(Key, DocId),
+        case {PassedEnd, AccCount, AccSkip, Resp} of
+        {true, _, _, _} ->
+            % The stop key has been passed, stop looping.
+            {stop, {AccCount, AccSkip, Resp, AccRevRows}};
+        {_, 0, _, _} ->
+            % we've done "count" rows, stop foldling
+            {stop, {0, 0, Resp, AccRevRows}};
+        {_, _, AccSkip, _} when AccSkip > 0 ->
+            {ok, {AccCount, AccSkip - 1, Resp, AccRevRows}};
+        {_, _, _, undefined} ->
+            {ok, Resp2} = start_json_response(Req, 200),
+            JsonBegin = io_lib:format("{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n",
+                    [TotalViewCount, Offset]),
+            JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+            
+            send_chunk(Resp2, JsonBegin ++ ?JSON_ENCODE(JsonObj)),
+            {ok, {AccCount - 1, 0, Resp2, AccRevRows}};
+        {_, AccCount, _, Resp} when (AccCount > 0) ->
+            JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+            send_chunk(Resp, ",\r\n" ++  ?JSON_ENCODE(JsonObj)),
+            {ok, {AccCount - 1, 0, Resp, AccRevRows}}
+        end
+    end,
+    case Count > 0 of
+    true ->     PosCountFun;
+    false ->    NegCountFun
+    end.
+
+finish_view_fold(Req, TotalRows, FoldResult) ->
+    case FoldResult of
+    {ok, {_, _, undefined, _}} ->
+        % nothing found in the view, nothing has been returned
+        % send empty view
+        send_json(Req, 200, {[
+            {total_rows, TotalRows},
+            {rows, []}
+        ]});
+    {ok, {_, _, Resp, AccRevRows}} ->
+        % end the view
+        send_chunk(Resp, AccRevRows ++ "\r\n]}"),
+        end_json_response(Resp);
+    Error ->
+        throw(Error)
+    end.
\ No newline at end of file